cozyclay 2.0.0 → 2.1.0

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 (260) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +219 -38
  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 +6 -5
  19. package/bin/cozyclay.mjs +12 -3
  20. package/bin/live/cli.mjs +2 -3
  21. package/bin/live/client.mjs +1 -1
  22. package/bin/live/studio.mjs +5 -2
  23. package/bin/mcp-runtime.mjs +12 -2
  24. package/bin/telemetry-state.mjs +3 -3
  25. package/dist/app/index.html +6 -6
  26. package/dist/assets/analytics-B41FmPYI.js +1 -0
  27. package/dist/assets/app-Do-Og5Pu.css +1 -0
  28. package/dist/assets/app-DwP-tl2x.js +4920 -0
  29. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  30. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  31. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  32. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  33. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  34. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  35. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/index.html +328 -222
  38. package/dist/sitemap.xml +5 -5
  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 +801 -259
  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 +196 -3
  72. package/src/ardy/timeline.jsx +117 -29
  73. package/src/ardy/to-cskel27.js +0 -2
  74. package/src/ardy/waypoints.js +1 -4
  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 +43 -24
  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 +16 -3
  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 +89 -32
  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/generation.js +1 -1
  127. package/src/motion-store.js +4 -4
  128. package/src/motion-trail.js +5 -11
  129. package/src/multimodel-ingest.js +1 -3
  130. package/src/object-catalog.jsx +7 -5
  131. package/src/object-gizmo.jsx +45 -31
  132. package/src/object-path.js +310 -67
  133. package/src/object-travel.js +224 -0
  134. package/src/otio.js +8 -3
  135. package/src/panels/CharacterTransformPanel.jsx +24 -6
  136. package/src/panels/ObjectTransformPanel.jsx +4 -3
  137. package/src/panels/PoseImagePanel.jsx +205 -0
  138. package/src/panels/PromptBlocksPanel.jsx +3 -1
  139. package/src/panels/ProxyFigurePanel.jsx +56 -0
  140. package/src/panels/ReferenceImageField.jsx +1 -1
  141. package/src/panels/RigControlPanel.jsx +24 -3
  142. package/src/panels/RigPanel.jsx +2 -1
  143. package/src/panels/SculptPanel.jsx +405 -0
  144. package/src/panels/SubjectBox.jsx +1 -1
  145. package/src/panels/SubjectsPanel.jsx +0 -2
  146. package/src/panels/VideoCapturePanel.jsx +2 -1
  147. package/src/panels/pose-image.css +42 -0
  148. package/src/panels/pose.css +3 -0
  149. package/src/panels/sculpt.css +162 -0
  150. package/src/plan-diagram.js +184 -0
  151. package/src/planview.jsx +127 -80
  152. package/src/playground.js +3 -5
  153. package/src/pose-fade.js +77 -0
  154. package/src/pose-gizmo.css +23 -0
  155. package/src/pose-gizmo.js +96 -0
  156. package/src/posestudio.jsx +134 -83
  157. package/src/project-browser.css +53 -0
  158. package/src/project-browser.jsx +52 -3
  159. package/src/project.js +55 -24
  160. package/src/props.jsx +177 -36
  161. package/src/proxy-figure.jsx +69 -0
  162. package/src/range-pin-panel.jsx +0 -1
  163. package/src/receipt-findings.js +20 -0
  164. package/src/render-passes.js +0 -1
  165. package/src/resource-status.jsx +1 -1
  166. package/src/root-path.js +48 -0
  167. package/src/route-owner.js +52 -0
  168. package/src/scene-asset-cache.js +1 -2
  169. package/src/scene-assets.js +12 -4
  170. package/src/scene-fbx.js +1 -1
  171. package/src/scene-mesh-cache.js +0 -1
  172. package/src/scene-objects.js +319 -19
  173. package/src/scenes.js +62 -17
  174. package/src/sculpt-blueprint.js +65 -0
  175. package/src/sculpt-compare.js +384 -0
  176. package/src/sculpt-geometry.js +141 -0
  177. package/src/sculpt-mode.js +168 -0
  178. package/src/sculpt-recipe.js +385 -0
  179. package/src/sculpt-run.js +351 -0
  180. package/src/sculpt-stage.jsx +221 -0
  181. package/src/settings-menu.jsx +1 -1
  182. package/src/shell/BoardSlot.jsx +341 -0
  183. package/src/shell/BottomDock.jsx +58 -14
  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/TopBar.jsx +17 -3
  188. package/src/shell/ViewportToolbar.jsx +18 -3
  189. package/src/shell/dock.css +239 -0
  190. package/src/shell/glass-regions.css +3 -2
  191. package/src/shell/log-store.js +4 -4
  192. package/src/shell/mode.css +20 -0
  193. package/src/shell/studio-shell-context.js +4 -1
  194. package/src/shot-authoring.js +69 -6
  195. package/src/stable-items.js +1 -1
  196. package/src/stage-attach-frames.js +169 -0
  197. package/src/stage-canvas-rigs.jsx +402 -0
  198. package/src/stage-capture.jsx +216 -0
  199. package/src/stage-characters.jsx +302 -0
  200. package/src/stage-editor-gizmos.jsx +1229 -0
  201. package/src/stage-labels-ko.js +204 -0
  202. package/src/stage-startup-defaults.js +182 -0
  203. package/src/starter-scenes.js +1 -1
  204. package/src/storyboard.js +24 -5
  205. package/src/studio-actions.js +42 -9
  206. package/src/studio-agent-commands.js +186 -23
  207. package/src/studio-agent-context.js +11 -9
  208. package/src/studio-agent-motion.js +2 -1
  209. package/src/studio-agent-protocol.js +56 -23
  210. package/src/studio-app-binding.js +52 -23
  211. package/src/studio-contact-sheet.js +3 -3
  212. package/src/studio-elements.js +11 -2
  213. package/src/studio-geometry-facts.js +114 -0
  214. package/src/styles.css +111 -10
  215. package/src/theme.js +1 -1
  216. package/src/timeline-fps.js +13 -0
  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 +110 -20
  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/kimodo/effector-constraints.mjs +4 -4
  248. package/tools/kimodo/generate.mjs +1 -1
  249. package/tools/knip-baseline.json +331 -0
  250. package/tools/loc-baseline.json +126 -0
  251. package/tools/process-supervisor.mjs +1 -1
  252. package/tools/projflow/replay.mjs +4 -4
  253. package/tools/projflow/runner.mjs +3 -3
  254. package/tools/qa/studio-control-count.mjs +2 -0
  255. package/tools/run-tests.mjs +96 -7
  256. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  257. package/dist/assets/app-C1sdVbg4.css +0 -1
  258. package/dist/assets/app-CA55tcI3.js +0 -4876
  259. package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
  260. package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/ui.jsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { createScrubGesture } from "./ui-scrub.js";
4
+ import { isImeComposing } from "./ime.js";
4
5
 
5
6
  // Custom dropdown, structurally matched to the reference (`.dropdown`,
6
7
  // `dd-caret`, `dropdown-menu`, `dropdown-item`, `dd-check`) with the keyboard
@@ -279,7 +280,7 @@ export function Field({ label, children }) {
279
280
  </div>
280
281
  );
281
282
  }
282
- export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title }) {
283
+ export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title, disabled = false }) {
283
284
  const [focused, setFocused] = useState(false);
284
285
  const [draft, setDraft] = useState(null);
285
286
  // The draft must be readable synchronously: blur() fires the commit
@@ -335,7 +336,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
335
336
  };
336
337
 
337
338
  const onKeyDown = (e) => {
338
- if (e.key === "Enter") {
339
+ if (e.key === "Enter" && !isImeComposing(e)) {
339
340
  e.preventDefault();
340
341
  commitDraft();
341
342
  // Keep focus and select the committed value so the next keystroke
@@ -357,7 +358,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
357
358
  // alone was a 15 px hotspot nobody found). Sensitivity is a fixed-travel
358
359
  // rate (src/ui-scrub.js), not pixels * step, so every field feels the same.
359
360
  const onFieldPointerDown = (e) => {
360
- if (e.button !== 0) return;
361
+ if (e.button !== 0 || disabled) return;
361
362
  // A press inside an already-focused input is text editing (caret
362
363
  // placement, drag-select), not a scrub — leave it alone.
363
364
  if (focused && e.target === inputRef.current) return;
@@ -460,7 +461,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
460
461
 
461
462
  return (
462
463
  <span
463
- className={`number-field v2-details-number-field${focused ? " editing" : ""}`}
464
+ className={`number-field v2-details-number-field${focused ? " editing" : ""}${disabled ? " disabled" : ""}`}
464
465
  title={title}
465
466
  onPointerDown={onFieldPointerDown}
466
467
  onPointerUp={endFieldDrag}
@@ -472,6 +473,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
472
473
  type="text"
473
474
  inputMode="decimal"
474
475
  value={display}
476
+ disabled={disabled}
475
477
  onFocus={onFocus}
476
478
  onBlur={onBlur}
477
479
  onChange={(e) => setDraftValue(e.target.value)}
@@ -0,0 +1,60 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { ASSET_DELETE_UNDO_MS } from "./app-stage.jsx";
3
+ import { CUTOUT_KIND, MESH_KIND } from "./scene-objects.js";
4
+ import { assetGraphSignature } from "./scene-assets.js";
5
+
6
+ export function useAssetShelf({ scenes, activeSceneId, sceneObjects }) {
7
+ // The imported-pictures region of the Assets shelf. null = the scan has
8
+ // never resolved (the pane shows skeletons, NEVER the empty message); an
9
+ // array = the SOURCE ids to show, mattes and cut renders already filtered
10
+ // out (asset-shelf.js). Scans run only while the shelf is visible, and
11
+ // re-run when a cutout's lineage changes — not on every transform tick, so
12
+ // a gizmo drag never hammers IndexedDB.
13
+ const [shelfImageIds, setShelfImageIds] = useState(null);
14
+ const [shelfMeshIds, setShelfMeshIds] = useState(null);
15
+ const [manageAssetStorage, setManageAssetStorage] = useState(false);
16
+ // A separate scan preserves the source-only placement shelf while the
17
+ // manager exposes every unreachable stored record, including matte and cut
18
+ // derivatives orphaned with a deleted card.
19
+ const [unusedAssetIds, setUnusedAssetIds] = useState(null);
20
+ const [usedAssetIds, setUsedAssetIds] = useState(null);
21
+ const [usageCounts, setUsageCounts] = useState(new Map());
22
+ const assetShelfScanTokenRef = useRef(0);
23
+ const legacyDerivedIdsRef = useRef(new Set());
24
+ // This is deliberately session-only. Each entry is a complete IndexedDB
25
+ // record, so Undo can put it back byte-for-byte until the page is reloaded.
26
+ const [assetTrash, setAssetTrash] = useState([]);
27
+ // Same rule as the object deletion offer: the toast is a window, not a
28
+ // banner. Only the OFFER expires — the trashed record itself is the restore
29
+ // data, so it is deliberately kept for the session and never timed out.
30
+ const [assetUndoOffered, setAssetUndoOffered] = useState(false);
31
+ useEffect(() => {
32
+ if (!assetUndoOffered) return undefined;
33
+ const timer = setTimeout(() => setAssetUndoOffered(false), ASSET_DELETE_UNDO_MS);
34
+ return () => clearTimeout(timer);
35
+ }, [assetUndoOffered]);
36
+ const [deletingAssetId, setDeletingAssetId] = useState(null);
37
+ const cutoutLineage = useMemo(
38
+ () => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : []))),
39
+ [sceneObjects],
40
+ );
41
+ const meshLineage = useMemo(
42
+ () => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === MESH_KIND ? [object.assetId] : []))),
43
+ [sceneObjects],
44
+ );
45
+ const projectCutoutLineage = useMemo(() => {
46
+ const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
47
+ return JSON.stringify(
48
+ allScenes.flatMap((scene) =>
49
+ (Array.isArray(scene.objects) ? scene.objects : []).flatMap((object) =>
50
+ object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : [],
51
+ ),
52
+ ),
53
+ );
54
+ }, [scenes, activeSceneId, sceneObjects]);
55
+ const projectAssetGraphSignature = useMemo(() => {
56
+ const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
57
+ return assetGraphSignature(allScenes);
58
+ }, [scenes, activeSceneId, sceneObjects]);
59
+ return { shelfImageIds, setShelfImageIds, shelfMeshIds, setShelfMeshIds, manageAssetStorage, setManageAssetStorage, unusedAssetIds, setUnusedAssetIds, usedAssetIds, setUsedAssetIds, usageCounts, setUsageCounts, assetShelfScanTokenRef, legacyDerivedIdsRef, assetTrash, setAssetTrash, assetUndoOffered, setAssetUndoOffered, deletingAssetId, setDeletingAssetId, cutoutLineage, meshLineage, projectCutoutLineage, projectAssetGraphSignature };
60
+ }
@@ -0,0 +1,50 @@
1
+ import { useRef, useState } from "react";
2
+ import { cameraTutorialSuppressed } from "./first-shot-handoff.js";
3
+ import { loadProjectSession } from "./project.js";
4
+ import { SCENES_STORAGE_KEY } from "./scenes.js";
5
+
6
+ export function useCameraTutorial({ embedMode, playgroundMode }) {
7
+ // The camera tutorial (#206): the landing page's seven steps, run against
8
+ // the real studio instead of the playground iframe. It opens from
9
+ // /app/?tutorial=camera or from Settings ▾, and it is not offered inside an
10
+ // embed.
11
+
12
+ // Both doors go through startCameraTutorial (#209), which puts the studio in
13
+ // the state the landing page teaches these steps in — the city-block starter
14
+ // scene with the walk take on its character — so Shot / Rail / Play always
15
+ // have something to frame. That function is declared with the project
16
+ // actions further down; the listeners here reach it through a ref so they
17
+ // always call the current render's closure (the confirm reads projectDirty).
18
+ const [cameraTutorialEntry] = useState(() => {
19
+ const params = new URLSearchParams(globalThis.location?.search || "");
20
+ const query = params.get("tutorial") === "camera";
21
+ let firstRun = false;
22
+ try {
23
+ firstRun = !query && !params.get("scene") && !cameraTutorialSuppressed()
24
+ && !loadProjectSession()?.name && !globalThis.localStorage?.getItem(SCENES_STORAGE_KEY);
25
+ } catch { firstRun = false; }
26
+ return !embedMode && !playgroundMode && !cameraTutorialSuppressed() && (query ? "query" : firstRun ? "first-run" : null);
27
+ });
28
+ const cameraTutorialQuery = Boolean(cameraTutorialEntry);
29
+ const [cameraTutorial, setCameraTutorial] = useState(false);
30
+ const cameraTutorialAnalytics = useRef(null);
31
+ const [cameraTutorialAttempt, setCameraTutorialAttempt] = useState(0);
32
+ const [cameraTutorialHandoff, setCameraTutorialHandoff] = useState(null);
33
+ const cameraTutorialCompletedRef = useRef(false);
34
+ const exportMenuTriggerRef = useRef(null);
35
+ const exportShotIdRef = useRef(null);
36
+ // The step the tutorial is on, published on the .app root so styles.css can
37
+ // spotlight the one control that step needs (#211).
38
+ const [cameraTutorialStep, setCameraTutorialStep] = useState(null);
39
+ const startCameraTutorialRef = useRef(null);
40
+ const cameraTutorialStarted = useRef(false);
41
+ // A tutorial restart never reloads the starter over the user's edits.
42
+ const tutorialStarterRef = useRef(false);
43
+ const tutorialLoadingRef = useRef(false);
44
+ const tutorialProjectEpochRef = useRef(0);
45
+ const tutorialSeedEpochRef = useRef(null);
46
+ // Armed once the starter is applied, consumed by the seed effect next to the
47
+ // hosted-demo seed as soon as the new character's rig exists.
48
+ const [tutorialSeedPending, setTutorialSeedPending] = useState(false);
49
+ return { cameraTutorialEntry, cameraTutorialQuery, cameraTutorial, setCameraTutorial, cameraTutorialAnalytics, cameraTutorialAttempt, setCameraTutorialAttempt, cameraTutorialHandoff, setCameraTutorialHandoff, cameraTutorialCompletedRef, exportMenuTriggerRef, exportShotIdRef, cameraTutorialStep, setCameraTutorialStep, startCameraTutorialRef, cameraTutorialStarted, tutorialStarterRef, tutorialLoadingRef, tutorialProjectEpochRef, tutorialSeedEpochRef, tutorialSeedPending, setTutorialSeedPending };
50
+ }
@@ -0,0 +1,12 @@
1
+ import { useRef, useState } from "react";
2
+ import { isPlaygroundEmbed } from "./playground.js";
3
+
4
+ export function useEmbedBridge() {
5
+ const embedMode = ["scene", "playview"].includes(new URLSearchParams(globalThis.location?.search || "").get("embed"));
6
+ // The landing page's try-it iframe: full studio interaction on a preset
7
+ // scene with the project chrome hidden and saving off (see playground.js).
8
+ const playgroundMode = isPlaygroundEmbed(globalThis.location?.search);
9
+ const [playgroundHint, setPlaygroundHint] = useState(null);
10
+ const playgroundExportRef = useRef(null);
11
+ return { embedMode, playgroundMode, playgroundHint, setPlaygroundHint, playgroundExportRef };
12
+ }
@@ -0,0 +1,7 @@
1
+ import { useState } from "react";
2
+
3
+ export function useFirstSuccessPrompt() {
4
+ const [firstSuccessGuideOpen, setFirstSuccessGuideOpen] = useState(false);
5
+ const [useCaseAskOpen, setUseCaseAskOpen] = useState(false);
6
+ return { firstSuccessGuideOpen, setFirstSuccessGuideOpen, useCaseAskOpen, setUseCaseAskOpen };
7
+ }
@@ -0,0 +1,15 @@
1
+ import { useState } from "react";
2
+
3
+ export function useI2vCaptureModal() {
4
+ const [i2vMotionEnabled, setI2vMotionEnabled] = useState(false);
5
+ const [i2vMotionMode, setI2vMotionMode] = useState("interpolate");
6
+ const [i2vMotionCameraUnlocked, setI2vMotionCameraUnlocked] = useState(false);
7
+ return {
8
+ i2vMotionEnabled,
9
+ setI2vMotionEnabled,
10
+ i2vMotionMode,
11
+ setI2vMotionMode,
12
+ i2vMotionCameraUnlocked,
13
+ setI2vMotionCameraUnlocked,
14
+ };
15
+ }
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createAgentTransport } from "./workflow/agent-client.js";
3
+ import { requestAgentImage, stylizeErrorMessage } from "./agent-image-client.js";
4
+ import { blueprintErrorMessage, blueprintRequest } from "./sculpt-blueprint.js";
5
+ import { agentSessionReady, createCommandTap, sculptModelChoice, sculptTargetFromSelection, storeSculptModel } from "./sculpt-mode.js";
6
+ import { SCULPT_KIND } from "./scene-objects.js";
7
+
8
+ export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId }) {
9
+ // Sculpt mode (#739): one sculpt on an isolated turntable, the Sculpt panel
10
+ // and its agent run. The target is the sculpt selected on entry, or a new one
11
+ // the first run creates; picking another sculpt while here retargets.
12
+ const sculptMode = workflowMode === "sculpt";
13
+ const sculptCamRef = useRef(null);
14
+ const [sculptTargetId, setSculptTargetId] = useState(null);
15
+ const [sculptView, setSculptView] = useState({ view: "front", nonce: 0 });
16
+ const [sculptReference, setSculptReference] = useState(null);
17
+ const [sculptRun, setSculptRun] = useState(null);
18
+ const sculptRunRef = useRef(null);
19
+ const [sculptAgentStatus, setSculptAgentStatus] = useState("unknown");
20
+ // The model Sculpt runs on (its own pick, remembered apart from the chat model) and the list it comes from.
21
+ const [sculptModels, setSculptModels] = useState({ state: "loading", providers: [], models: [] });
22
+ const [sculptModelPick, setSculptModelPick] = useState({ model: "", effort: null });
23
+ const chooseSculptModel = (next) => setSculptModelPick((current) => {
24
+ const choice = { ...current, ...next };
25
+ storeSculptModel(choice);
26
+ return choice;
27
+ });
28
+ const [sculptTransport] = useState(() => createAgentTransport({ surface: "studio" }));
29
+ const [sculptTap] = useState(createCommandTap);
30
+ const sculptTarget = sceneObjects.find((object) => object.id === sculptTargetId && object.renderer === SCULPT_KIND) ?? null;
31
+ const requestSculptView = (view) => setSculptView((current) => ({ view, nonce: current.nonce + 1 }));
32
+ useEffect(() => {
33
+ if (!sculptMode) return;
34
+ setSculptTargetId(sculptTargetFromSelection(sceneObjects, selectedSceneObjectId));
35
+ requestSculptView("front");
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ }, [sculptMode]);
38
+ useEffect(() => {
39
+ const selected = sculptMode ? sculptTargetFromSelection(sceneObjects, selectedSceneObjectId) : null;
40
+ if (selected) setSculptTargetId(selected);
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [selectedSceneObjectId]);
43
+ // Start needs the Agent panel's readiness rule (signed in, or a provider
44
+ // key in the sidecar); a sign-in finished in another window lands on focus.
45
+ useEffect(() => {
46
+ if (!sculptMode) return undefined;
47
+ let live = true;
48
+ const read = () => {
49
+ sculptTransport.status().then(
50
+ (status) => { if (live) setSculptAgentStatus(agentSessionReady(status) ? "ready" : "signed-out"); },
51
+ () => { if (live) setSculptAgentStatus("signed-out"); },
52
+ );
53
+ // The Agent panel's catalogue: a key added or removed elsewhere changes what can run.
54
+ sculptTransport.models().then(
55
+ (advertised) => {
56
+ if (!live) return;
57
+ const list = Array.isArray(advertised?.models) ? advertised.models : [];
58
+ setSculptModels({ state: list.length ? "ready" : "failed", providers: advertised?.providers ?? [], models: list });
59
+ setSculptModelPick((current) => sculptModelChoice(advertised, current));
60
+ },
61
+ () => { if (live) setSculptModels((current) => ({ ...current, state: current.models.length ? current.state : "failed" })); },
62
+ );
63
+ };
64
+ read();
65
+ window.addEventListener("focus", read);
66
+ return () => { live = false; window.removeEventListener("focus", read); };
67
+ }, [sculptMode, sculptTransport]);
68
+ // The blueprint step: the reference turned into a four-view model sheet by one image request.
69
+ const [sculptBlueprint, setSculptBlueprint] = useState({ state: "idle", pose: "keep", notes: "", sheet: null, error: null, startedAt: null });
70
+ const blueprintAbortRef = useRef(null);
71
+ async function generateSculptBlueprint() {
72
+ if (!sculptReference) return;
73
+ blueprintAbortRef.current?.abort();
74
+ const controller = new AbortController();
75
+ blueprintAbortRef.current = controller;
76
+ const startedAt = Date.now();
77
+ setSculptBlueprint((current) => ({ ...current, state: "generating", error: null, startedAt }));
78
+ try {
79
+ const result = await requestAgentImage(blueprintRequest({ reference: sculptReference, pose: sculptBlueprint.pose, notes: sculptBlueprint.notes }), { signal: controller.signal });
80
+ if (blueprintAbortRef.current !== controller) return;
81
+ setSculptBlueprint((current) => ({ ...current, state: "ready", sheet: { dataUrl: result.dataUrl, width: result.width, height: result.height, ms: Date.now() - startedAt } }));
82
+ } catch (error) {
83
+ if (blueprintAbortRef.current !== controller) return;
84
+ const cancelled = controller.signal.aborted;
85
+ setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle", error: cancelled ? null : blueprintErrorMessage(error, stylizeErrorMessage(error).replace(/^Stylize failed — |^스타일화 실패 — /, "")) }));
86
+ }
87
+ }
88
+ function cancelSculptBlueprint() {
89
+ const controller = blueprintAbortRef.current;
90
+ blueprintAbortRef.current = null;
91
+ controller?.abort();
92
+ setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle" }));
93
+ }
94
+ return { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint };
95
+ }
@@ -8,6 +8,9 @@ import {
8
8
  attachmentFromFile,
9
9
  transferCarriesFiles,
10
10
  } from "./attachment-image.js";
11
+ import { isImeComposing } from "../ime.js";
12
+ import { useStudioShell } from "../shell/studio-shell-context.js";
13
+ import { ko } from "../locale.js";
11
14
  import {
12
15
  AGENT_PANEL_OVERLAY_BREAKPOINT,
13
16
  AGENT_PANEL_RAIL_WIDTH,
@@ -34,9 +37,14 @@ import {
34
37
  formatElapsed,
35
38
  formatResetIn,
36
39
  readStoredPanelWidth,
40
+ readStoredSupervisor,
37
41
  storeModel,
38
42
  storePanelWidth,
43
+ storeSupervisor,
39
44
  STUDIO_SESSION_STORAGE_KEY,
45
+ selectedModelRoles,
46
+ shortModelId,
47
+ supervisorFollowUp,
40
48
  } from "./agent-client.js";
41
49
  import "./agent-panel.css";
42
50
 
@@ -122,6 +130,18 @@ function PausedCard({ resetAt, onRetry, onSwitchModel }) {
122
130
  </div>;
123
131
  }
124
132
 
133
+ // The lineup the sidecar derived for the selected model (#717), read-only:
134
+ // what a turn may lean on beside it. Vision is named only when it is not the
135
+ // selected model itself.
136
+ function ModelRolesLine({ model, roles }) {
137
+ if (!roles) return null;
138
+ const parts = [[ko("helper", "보조"), roles.helper], [ko("advisor", "자문"), roles.advisor]];
139
+ if (roles.vision !== model) parts.push([ko("vision", "비전"), roles.vision]);
140
+ return <p className="agent-model-roles" data-agent-model-roles="true">
141
+ {parts.map(([label, key]) => `${label} · ${shortModelId(key)}`).join(" · ")}
142
+ </p>;
143
+ }
144
+
125
145
  // A generation is a server-owned job. The card shows the state the runtime
126
146
  // reported, the phase it named and only a progress value it actually sent.
127
147
  function JobCard({ job, onStop, onAccept }) {
@@ -182,6 +202,47 @@ function ReceiptCard({ item }) {
182
202
  </div>;
183
203
  }
184
204
 
205
+ const SUPERVISOR_SEVERITY_COPY = { blocker: ko("blocker", "차단"), concern: ko("concern", "우려"), note: ko("note", "참고") };
206
+ const SUPERVISOR_STATUS_COPY = { done: ko("done", "완료"), partial: ko("partial", "일부"), refused: ko("refused", "거부됨"), missing: ko("missing", "누락") };
207
+
208
+ // The supervisor's note on a finished Studio turn (#728): muted, collapsed to
209
+ // its summary and counts, and never acted on by itself. Its one action puts
210
+ // the blockers and concerns into the composer as the author's next turn.
211
+ function SupervisorCard({ note, onSend, defaultOpen = false }) {
212
+ const [open, setOpen] = useState(defaultOpen);
213
+ if (note.verdict !== "reviewed") return <div className="agent-card agent-supervisor-card" data-supervisor-verdict="unavailable">
214
+ <p className="agent-supervisor-summary">{ko("Supervisor review unavailable", "감독 검토를 할 수 없었어요")}{note.reason ? ` — ${note.reason}` : ""}</p>
215
+ </div>;
216
+ const followUp = supervisorFollowUp(note);
217
+ // Validated run_action drafts (#743), shown under the issue each one fixes.
218
+ const fixes = Array.isArray(note.fixes) ? note.fixes : [];
219
+ return <div className="agent-card agent-supervisor-card" data-supervisor-verdict="reviewed" data-open={open ? "true" : "false"}>
220
+ <div className="agent-supervisor-head">
221
+ <span className="agent-job-label">{ko("Supervisor", "감독")}</span>
222
+ {["blocker", "concern", "note"].filter((severity) => note.counts?.[severity] > 0).map((severity) => <span key={severity} className={`agent-job-badge agent-supervisor-badge ${severity}`} data-severity={severity}>{note.counts[severity]} {SUPERVISOR_SEVERITY_COPY[severity]}</span>)}
223
+ <button type="button" className="agent-ghost-button agent-supervisor-toggle" aria-expanded={open} onClick={() => setOpen((value) => !value)}>{open ? ko("Less", "접기") : ko("Details", "자세히")}</button>
224
+ </div>
225
+ <p className="agent-supervisor-summary">{note.summary}</p>
226
+ {open && note.items?.length > 0 && <ul className="agent-receipt-notes agent-supervisor-items">
227
+ {note.items.map((item, index) => <li key={index} data-status={item.status}>{SUPERVISOR_STATUS_COPY[item.status] ?? item.status} · {item.text}{item.evidence ? ` — ${item.evidence}` : ""}</li>)}
228
+ </ul>}
229
+ {open && note.issues?.length > 0 && <ul className="agent-receipt-notes agent-supervisor-issues">
230
+ {note.issues.map((issue, index) => {
231
+ const fix = fixes.find((entry) => entry.issueIndex === index);
232
+ return <li key={index} data-severity={issue.severity}>{SUPERVISOR_SEVERITY_COPY[issue.severity] ?? issue.severity} · {issue.text}{issue.evidence ? ` — ${issue.evidence}` : ""}
233
+ {fix && <code className="agent-supervisor-fix" data-fix-action={fix.action}>{`run_action ${JSON.stringify({ action: fix.action, args: fix.args })}`}</code>}
234
+ </li>;
235
+ })}
236
+ </ul>}
237
+ {followUp && <div className="agent-job-actions">
238
+ <button type="button" className="agent-ghost-button agent-supervisor-send" onClick={() => onSend(followUp)}
239
+ title={note.autofix?.eligible ? ko("Autofix is on: the blocker fixes would be applied as written", "자동 수정 켜짐: 차단 수정안을 적힌 그대로 적용합니다") : undefined}>
240
+ {fixes.length ? ko("Apply fixes as next prompt", "수정안 다음 턴으로 보내기") : ko("Send as next prompt", "다음 턴으로 보내기")}
241
+ </button>
242
+ </div>}
243
+ </div>;
244
+ }
245
+
185
246
  const RECOVERY_COPY = {
186
247
  none: "No recovery is available for this command.",
187
248
  inspect: "Ask the agent to inspect the target before trying again.",
@@ -286,6 +347,11 @@ export default function AgentPanel({
286
347
  const transport = useMemo(() => injectedTransport || createAgentTransport({ surface }), [injectedTransport, surface]);
287
348
  const mockState = transport.mock ? transport.state : null;
288
349
 
350
+ // Inside the studio the title names the project's previs mode (#650).
351
+ const previsMode = useStudioShell()?.previsMode;
352
+ const titleMode = surface === "studio" && previsMode
353
+ ? (previsMode === "storyboard" ? ko("Storyboard", "스토리보드") : ko("Animation", "애니메이션"))
354
+ : null;
289
355
  const [collapsed, setCollapsed] = useState(defaultCollapsed);
290
356
  // Only the dock owns a width; an embedded host sizes the panel itself and
291
357
  // never reads the dock's stored key.
@@ -302,6 +368,8 @@ export default function AgentPanel({
302
368
  const [modelProviders, setModelProviders] = useState([]);
303
369
  const [model, setModel] = useState("");
304
370
  const [modelsState, setModelsState] = useState("loading");
371
+ // The roles derived per main model (#717); shown, never sent or stored.
372
+ const [modelRoles, setModelRoles] = useState(null);
305
373
  // null = the model's backend default; picking a model resets it.
306
374
  const [effort, setEffort] = useState(null);
307
375
  const efforts = useMemo(() => effortOptions(models.find((entry) => entry.id === model)), [models, model]);
@@ -309,6 +377,10 @@ export default function AgentPanel({
309
377
  // remembered so a reload does not quietly move the next turn to another
310
378
  // provider's model.
311
379
  const chooseModel = useCallback((id) => { setModel(id); setEffort(null); storeModel(id); }, []);
380
+ // The supervisor switch (#728): a preference like the model pick. Off skips
381
+ // the post-turn review for every turn sent from this panel.
382
+ const [supervisorOn, setSupervisorOn] = useState(() => readStoredSupervisor());
383
+ const toggleSupervisor = useCallback(() => setSupervisorOn((current) => { storeSupervisor(!current); return !current; }), []);
312
384
  // One place takes `{ providers, models }` from the sidecar: the grouped list
313
385
  // the dropdown draws, the flat key-addressed list every lookup uses, and the
314
386
  // model to open with. Returns false when nothing was advertised.
@@ -318,6 +390,7 @@ export default function AgentPanel({
318
390
  const providers = Array.isArray(advertised?.providers) ? advertised.providers : [];
319
391
  setModelProviders(providers);
320
392
  setModels(list);
393
+ setModelRoles(advertised?.roles?.byMain ?? null);
321
394
  // A saved or removed key arrives through this same path, so a selection the
322
395
  // panel opened with is dropped here the moment its provider cannot run it.
323
396
  setModel((current) => nextSelectedModel(providers, list, current));
@@ -345,6 +418,7 @@ export default function AgentPanel({
345
418
  const [restoreReady, setRestoreReady] = useState(surface !== "studio");
346
419
 
347
420
  const composerRef = useRef(null);
421
+ const focusPendingRef = useRef(false);
348
422
  const transcriptRef = useRef(null);
349
423
 
350
424
  // The host's context builder and image-action handler are read through refs
@@ -382,10 +456,12 @@ export default function AgentPanel({
382
456
  const payload = await transport.loadSession(stored);
383
457
  if (!Array.isArray(payload?.transcript)) throw new Error("missing transcript");
384
458
  if (!cancelled) store.restore(payload.transcript, payload.sessionId || stored);
385
- } catch {
459
+ } catch (error) {
386
460
  if (!cancelled) {
387
461
  store.newSession();
388
- setRestoreNotice("Previous conversation could not be restored");
462
+ // A session id with no record behind it (the pane opened, nothing
463
+ // was ever sent) is nothing to restore, not a failure.
464
+ if (error?.status !== 404) setRestoreNotice("Previous conversation could not be restored");
389
465
  }
390
466
  } finally {
391
467
  if (!cancelled) setRestoreReady(true);
@@ -399,7 +475,6 @@ export default function AgentPanel({
399
475
  try { globalThis.localStorage?.setItem(STUDIO_SESSION_STORAGE_KEY, chat.sessionId); } catch { /* storage may be unavailable */ }
400
476
  }, [chat.sessionId, restoreReady, surface]);
401
477
 
402
- // --- activity line -----------------------------------------------------
403
478
  // One line that always states what the panel is doing, ticking while a turn
404
479
  // is live and holding the turn's outcome for a few seconds after it ends.
405
480
  const [activityNow, setActivityNow] = useState(() => Date.now());
@@ -414,7 +489,6 @@ export default function AgentPanel({
414
489
  return () => clearInterval(timer);
415
490
  }, [activity.ticking]);
416
491
 
417
- // --- session bootstrap -------------------------------------------------
418
492
  // Two ways in (#379): the ChatGPT sign-in, or a provider key the sidecar
419
493
  // already holds. `providersConfigured` counts the second, so a session that
420
494
  // never signed in to ChatGPT still gets a composer instead of a sign-in wall
@@ -518,7 +592,6 @@ export default function AgentPanel({
518
592
  store.send("Give me a wide two-shot of this scene", { attachFrame: false, model });
519
593
  }, [authState, items.length, mockState, model, store]);
520
594
 
521
- // --- layout ------------------------------------------------------------
522
595
  useEffect(() => {
523
596
  if (embedded) return;
524
597
  const onResize = () => setOverlay((globalThis.innerWidth || 1440) < AGENT_PANEL_OVERLAY_BREAKPOINT);
@@ -555,7 +628,10 @@ export default function AgentPanel({
555
628
  // Focus moves to the composer whenever the panel opens. The composer only
556
629
  // mounts once the session resolves, so authState is a dependency too:
557
630
  // otherwise this fires against a composer that does not exist yet. A hidden
558
- // embedded panel never pulls focus out of the host's own surface.
631
+ // embedded panel never pulls focus out of the host's own surface. An
632
+ // embedded panel never collapses itself (its host hides it), so shown means
633
+ // open there: without that the Studio composer opened unfocused and the
634
+ // next Cmd+V landed on the stage.
559
635
  useEffect(() => {
560
636
  if (hidden) {
561
637
  // Chrome blurs a hidden focused field only on its next lifecycle step;
@@ -563,8 +639,12 @@ export default function AgentPanel({
563
639
  if (composerRef.current && composerRef.current === document.activeElement) composerRef.current.blur();
564
640
  return;
565
641
  }
566
- if (!collapsed) composerRef.current?.focus();
567
- }, [authState, collapsed, hidden]);
642
+ if (!(embedded || !collapsed)) return;
643
+ composerRef.current?.focus();
644
+ // A composer still waiting on the model list is disabled and refuses
645
+ // focus; the open is remembered so it lands once the composer enables.
646
+ focusPendingRef.current = document.activeElement !== composerRef.current;
647
+ }, [authState, collapsed, embedded, hidden]);
568
648
 
569
649
  useEffect(() => {
570
650
  const node = transcriptRef.current;
@@ -595,12 +675,13 @@ export default function AgentPanel({
595
675
  });
596
676
  }, []);
597
677
 
598
- // --- pasted and dropped pictures ---------------------------------------
599
678
  // A screenshot is the fastest thing an author can show the agent, so the
600
679
  // composer takes one from the clipboard or the desktop. Only a transfer that
601
680
  // actually carries a picture is intercepted: a text paste still lands in the
602
681
  // caret, which is the whole reason the studio's document handler steps aside
603
- // for a textarea in the first place.
682
+ // for a textarea in the first place. The paste is taken on the composer
683
+ // block, not the textarea, so the Send and model buttons a click just left
684
+ // focus on do not hand the picture to the stage.
604
685
  const attachFiles = useCallback(async (files) => {
605
686
  const prepared = [];
606
687
  let failed = 0;
@@ -610,7 +691,6 @@ export default function AgentPanel({
610
691
  const rejected = prepared.length ? store.addAttachments(prepared).rejected : 0;
611
692
  setAttachNotice(rejected ? ATTACHMENT_LIMIT_NOTICE : failed ? ATTACHMENT_FAILED_NOTICE : null);
612
693
  }, [store]);
613
- /** Whether this transfer belongs to the composer. */
614
694
  const takeTransfer = useCallback((transfer) => {
615
695
  const { images, unsupported } = attachmentFilesFromTransfer(transfer);
616
696
  if (!images.length) {
@@ -641,12 +721,11 @@ export default function AgentPanel({
641
721
  if (takeTransfer(event.dataTransfer)) event.preventDefault();
642
722
  }, [takeTransfer]);
643
723
 
644
- // --- turn --------------------------------------------------------------
645
724
  const runTurn = useCallback((text) => {
646
725
  setAttachNotice(null);
647
726
  setSteerNotice("");
648
- return store.send(text, { attachFrame, model, effort: effort ?? undefined });
649
- }, [attachFrame, effort, model, store]);
727
+ return store.send(text, { attachFrame, model, effort: effort ?? undefined, supervisor: supervisorOn });
728
+ }, [attachFrame, effort, model, store, supervisorOn]);
650
729
  const stopTurn = useCallback(() => store.stop(), [store]);
651
730
  // Steering the turn that is already running: the composer's text joins THAT
652
731
  // turn instead of starting a second one. A refusal (the turn ended while the
@@ -715,7 +794,6 @@ export default function AgentPanel({
715
794
  }
716
795
  }, [store, transport]);
717
796
 
718
- // --- provider keys -----------------------------------------------------
719
797
  // The section is read when it opens; every write goes back through the
720
798
  // refresh above, because a key changes the session, not just a row.
721
799
  const toggleProviderKeys = useCallback(() => {
@@ -759,7 +837,7 @@ export default function AgentPanel({
759
837
  stopTurn();
760
838
  return;
761
839
  }
762
- if (event.key === "Enter" && !event.shiftKey) {
840
+ if (event.key === "Enter" && !isImeComposing(event) && !event.shiftKey) {
763
841
  event.preventDefault();
764
842
  // Enter does what the button under it says: Send, or Steer while a turn
765
843
  // on a steerable surface is still running.
@@ -782,6 +860,13 @@ export default function AgentPanel({
782
860
  // can send to: the composer stays shut instead of submitting a turn the
783
861
  // provider would refuse.
784
862
  const composerDisabled = panelState === "rate-limited" || !model || !modelIsSelectable(modelProviders, model);
863
+ useEffect(() => {
864
+ if (hidden) focusPendingRef.current = false;
865
+ if (hidden || composerDisabled || !focusPendingRef.current) return;
866
+ focusPendingRef.current = false;
867
+ // Only claim focus nobody else has taken in the meantime.
868
+ if (!document.activeElement || document.activeElement === document.body) composerRef.current?.focus();
869
+ }, [composerDisabled, hidden]);
785
870
 
786
871
  if (collapsed && !embedded) {
787
872
  return <aside className="agent-panel collapsed" data-agent-state={panelState} data-agent-collapsed="true" aria-label="Agent panel, collapsed">
@@ -819,7 +904,7 @@ export default function AgentPanel({
819
904
 
820
905
  <header className="agent-header">
821
906
  <StatusDot tone={statusTone} title={panelState} />
822
- <h2 className="agent-title">Agent</h2>
907
+ <h2 className="agent-title" data-previs-mode={titleMode ? previsMode : undefined}>{titleMode ? `Agent · ${titleMode}` : "Agent"}</h2>
823
908
  <span className="agent-header-spacer" />
824
909
  <button type="button" className="agent-icon-button agent-new" aria-label="New conversation" title="New conversation" onClick={newSession}><FiPlus size={14} aria-hidden="true" /></button>
825
910
  {presentation.history && <span className="agent-history-wrap">
@@ -904,6 +989,7 @@ export default function AgentPanel({
904
989
  if (item.kind === "job") return <div className="agent-row" key={item.id}><JobCard job={item} onStop={stopTurn} onAccept={(job) => store.acceptJob(job.jobId)} /></div>;
905
990
  if (item.kind === "receipt") return <div className="agent-row" key={item.id}><ReceiptCard item={item} /></div>;
906
991
  if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt)} /></div>;
992
+ if (item.kind === "supervisor") return <div className="agent-row" key={item.id}><SupervisorCard note={item} onSend={(text) => { store.setDraft(text); runTurn(text); }} /></div>;
907
993
  return <div className="agent-row" key={item.id}><ImageResultCard image={item} onUse={(image) => store.applyImage(image.id)} onUndo={(image) => store.undoImage(image.id)} onRegenerate={() => runTurn(chat.lastPrompt)} onOpen={setLightbox} /></div>;
908
994
  })}
909
995
 
@@ -923,6 +1009,7 @@ export default function AgentPanel({
923
1009
  onDragOver={composerDisabled ? undefined : onComposerDragOver}
924
1010
  onDragLeave={composerDisabled ? undefined : onComposerDragLeave}
925
1011
  onDrop={composerDisabled ? undefined : onComposerDrop}
1012
+ onPaste={composerDisabled ? undefined : onComposerPaste}
926
1013
  >
927
1014
  {pendingAttachments.length > 0 && <ul className="agent-attachments" aria-label="Attached images">
928
1015
  {pendingAttachments.map((attachment) => <li key={attachment.id} className="agent-attachment">
@@ -931,8 +1018,6 @@ export default function AgentPanel({
931
1018
  </li>)}
932
1019
  </ul>}
933
1020
  {attachNotice && <p className="agent-attachment-notice" role="status">{attachNotice}</p>}
934
- {/* One field: the message, and under it on the same surface the
935
- frame toggle, the model and effort picks, and Send. */}
936
1021
  <div className="agent-field">
937
1022
  <textarea
938
1023
  ref={composerRef}
@@ -943,7 +1028,6 @@ export default function AgentPanel({
943
1028
  disabled={composerDisabled}
944
1029
  onChange={(event) => store.setDraft(event.target.value)}
945
1030
  onKeyDown={onComposerKeyDown}
946
- onPaste={onComposerPaste}
947
1031
  onDragOver={onComposerDragOver}
948
1032
  onDrop={onComposerDrop}
949
1033
  />
@@ -972,6 +1056,11 @@ export default function AgentPanel({
972
1056
  {efforts.map((value, index) => <option key={value} value={value} title={index === 0 ? "Default for this model" : undefined}>{value}</option>)}
973
1057
  </select>
974
1058
  )}
1059
+ {surface === "studio" && (
1060
+ <button type="button" className={`agent-supervisor-toggle${supervisorOn ? " is-on" : ""}`} role="switch" aria-checked={supervisorOn} aria-label={ko("Supervisor review", "감독 검토")} title={ko(supervisorOn ? "Supervisor review is on: a note arrives after each editing turn" : "Supervisor review is off", supervisorOn ? "감독 검토 켜짐: 편집 턴이 끝나면 메모가 옵니다" : "감독 검토 꺼짐")} onClick={toggleSupervisor}>
1061
+ {ko("Supervisor", "감독")} {supervisorOn ? "ON" : "OFF"}
1062
+ </button>
1063
+ )}
975
1064
  <span className="agent-composer-spacer" aria-hidden="true" />
976
1065
  {surface === "studio" && i2vActionRef.current && <button
977
1066
  type="button"
@@ -993,6 +1082,7 @@ export default function AgentPanel({
993
1082
  </>
994
1083
  : <button type="button" className="agent-send" aria-label="Send" title="Send (Enter)" disabled={composerDisabled || !draft.trim()} onClick={() => runTurn(draft)}><FiArrowUp size={14} aria-hidden="true" /></button>}
995
1084
  </div>
1085
+ <ModelRolesLine model={model} roles={selectedModelRoles(modelRoles, model)} />
996
1086
  </div>
997
1087
  </div>}
998
1088
  {/* The hint exists to warn about image spend; a surface that cannot
@@ -14,7 +14,7 @@ import { Toaster, toast } from "react-hot-toast";
14
14
  import { loadWorkflowGraph, normalizeWorkflowGraph, storeWorkflowGraph, WORKFLOW_STORAGE_KEY } from "../project.js";
15
15
  import AgentPanel from "./AgentPanel.jsx";
16
16
  import CozySceneNode from "./CozySceneNode.jsx";
17
- import { applyCozyScenePatch, normalizeCozySceneData, sceneConnectionAllowed, toCozySceneRunRequest } from "./cozy-scene-node.js";
17
+ import { applyCozyScenePatch, normalizeCozySceneData, sceneConnectionAllowed } from "./cozy-scene-node.js";
18
18
  import { activeSceneCharacters, characterHandleId, characterIdFromHandle, normalizeMotionInputData, motionInputOutput } from "./motion-input.js";
19
19
  import { DEFAULT_NODE_SCHEMAS, defaultFormValues, schemaCategoryForType, schemaModelEntries, schemaProperties } from "./node-schema.js";
20
20
  import { executeLocalWorkflowGraph } from "./local-workflow.js";