cozyclay 1.9.0 → 2.0.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 (286) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/LIVE-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. package/src/scene-history.js +0 -129
package/CHANGELOG.md CHANGED
@@ -1,5 +1,176 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ CozyClay 2.0 is the v2 editor: a floating-glass shell with light and dark
6
+ themes, one four-mode Studio (Stage, Pose, Camera, Motion on keys 1-4), and
7
+ the v2 Outliner, Sequencer, start screen, Details panels and Preferences.
8
+ Under it, every document edit, whether it comes from the UI, the Agent panel,
9
+ MCP or the CLI, now runs through one command bus on a document store with one
10
+ undo history, so anything the editor can do is callable by an agent without
11
+ separate wiring. The Studio Agent panel in the npm package works again
12
+ (#576). **No MCP tool or CLI command was removed**: legacy MCP tools are
13
+ aliases of bus commands and answer the editor's receipts.
14
+
15
+ Studio
16
+
17
+ - Floating-glass shell: the viewport fills the window and the top bar, both
18
+ columns, the dock and the status strip float over it. Light and dark
19
+ themes, and the stage follows the theme live. The dock runs Animation |
20
+ Assets tabs sized to the Sequencer's lanes, with the shot preview docked at
21
+ its right end; the standing status row is gone and live export status
22
+ floats in the viewport. (#570, #572, #519)
23
+ - One four-mode switch, Stage, Pose, Camera and Motion, on keys 1-4. Pose
24
+ mode is the single way into IK (W/E/R pick ik, trail and pin there); the
25
+ timeline IK toggle and the Rig Control IK buttons are gone, and leaving
26
+ Pose ends IK. (#521)
27
+ - v2 Outliner: 36 px header with the item count, search, typed rows, amber
28
+ selection; right-click Create replaces the toolbar's + Add object. (#525)
29
+ - v2 Sequencer: header, readout and actions, a 190 px track tree, clip
30
+ fills, amber playhead, keys and pin bands, scrollable camera, rail and
31
+ crane rows. (#528)
32
+ - v2 start screen: project nav, template categories, recent rows, the
33
+ browser project store and a project preview; File › New opens the same
34
+ screen. (#531, #551)
35
+ - v2 Details panels and primitives: pose tools with an auto-fix preview,
36
+ motion details with takes, the viewport toolbar and overlays, the top bar,
37
+ menus and status bar, the content browser and log, and the Preferences
38
+ dialog. (#522, #523, #524, #526, #527, #529, #530, #532)
39
+ - Generate actions have one home: the top-bar Generate Motion button runs
40
+ every prompt block when any block has text. Copy follows v2: "ARDY" leaves
41
+ the user-facing text, pose toasts say "Pose mode", path endpoints read
42
+ PATH START / PATH END, the first-run guide says Outliner. (#550, #551)
43
+ - Top-View props recede so the camera, cast and rail read first. (#566)
44
+ - When WebGL is unavailable the stage says so instead of staying blank. (#518)
45
+ - 194 dead CSS rules are gone and the v2 information architecture is
46
+ recorded in `docs/studio-ui-ia.md`; Details, Outliner and top-bar props
47
+ that were never read are dropped. (#533, #552, #556, #558, #560, #562)
48
+ - Korean UI: 56 reversed `ko()` argument pairs in the motion card are fixed
49
+ (#505, by @romanr), as are the VideoCapturePanel quality-gate messages
50
+ (#565).
51
+
52
+ IK and motion
53
+
54
+ - IK and motion trails on the split architecture: delta IK drag baking over
55
+ the raw clip, per-key blend persistence, a remembered IK camera entry,
56
+ trail picking against the rendered rig, limb, head and root trail edits
57
+ with conflict warnings. (#511)
58
+ - Range pin with an exact hinge solve and body reach compensation, carried
59
+ through persistence, undo/redo, timeline bands and preview markers. (#515)
60
+ - Fit to platforms with Fit / Remove controls and a stable knee bend side,
61
+ verified over 784 cases with no reverse bends. (#517)
62
+ - One motion generation pipeline on the job contract: `motion.generate`
63
+ serves the UI, the Agent and MCP alike, with concurrent-edit parity (a
64
+ stale job publishes nothing, an unrelated object edit still installs),
65
+ Stop and cancel, reconnect, and receipt undo. The agent-only motion
66
+ pipeline is deleted. (#444, #452)
67
+ - Image-to-video motion is named `i2v` in code and "AI motion" in the UI;
68
+ "fal" names only the provider adapter. Command ids, analytics values and
69
+ persisted values are unchanged. (#509)
70
+
71
+ Command bus and document store
72
+
73
+ - Every document mutation runs through one command bus: wire transactions,
74
+ a scoped job fence, receipts, refusals and exposure. Action registrations,
75
+ the agent binding and the domain writers moved out of App.jsx. (#435,
76
+ #437, #459, #460, #477)
77
+ - A document store owns stage, objects, shots, cast, motion and IK, and
78
+ project and scenes, each routed through the bus, with collection element
79
+ kinds and cross-domain undo order. (#449, #447, #445, #441, #442, #448,
80
+ #461, #480)
81
+ - One store-owned undo history; the per-domain history owners are gone.
82
+ (#494) Duplicate agent paths are removed: `inspect_studio`, `run_action`
83
+ and `verify_result` are the three tool families, `operate_studio` is an
84
+ alias, and selection, playhead and view changes add no undo entries.
85
+ (#495) The writer baseline is zero and parity is complete: all 62
86
+ document-mutating UI handlers reach `run`, enforced by ratchet tests and
87
+ a generated parity matrix. (#436, #496)
88
+
89
+ Agent
90
+
91
+ - The Studio agent pane reaches the whole Studio through the action
92
+ registry the UI dispatches through, and every bus command is discoverable
93
+ and callable through agent `run_action`, MCP `studio_commands` /
94
+ `studio_run` and `cclay live commands` / `cclay live run` with no
95
+ agent-specific wiring. (#423, #438)
96
+ - Motion failures are visible and routable, `verify_result` reports
97
+ honestly, and a failed attempt no longer uses up the message's one
98
+ generation. (#426)
99
+ - Minimal Studio Agent panel: an icon-only head, the account under the ⋯
100
+ menu, one-line notices, an empty state with suggestion chips; it sits
101
+ under Details in the right column instead of replacing it. (#574, #570)
102
+ - In the npm package the Studio Agent panel loads its live tools from the
103
+ staged MCP runtime that `cclay mcp` installs once per version, so the live
104
+ hub starts and the pane works; `mcp/mesh-file.mjs` ships with the package
105
+ and `cclay mcp` starts again. A runtime that cannot be installed is
106
+ reported on the launcher's terminal. (#576)
107
+
108
+ MCP
109
+
110
+ - Legacy tools are thin aliases of bus commands and return the editor's JSON
111
+ receipts: revision before and after, retained undo and refusal codes.
112
+ `apply_batch` maps to `objects.batch` and keeps `atomic`, `stopOnError`
113
+ and one undo entry; replacing scene ids needs a Studio-issued confirmation
114
+ token. (#446)
115
+ - `initialize` waits the SDK's default 60 s. (#502)
116
+
117
+ Analytics
118
+
119
+ - Builder telemetry: pose edits, i2v motion A→B, `$exception`, a device
120
+ profile (GPU and frame rate), the install source from
121
+ `npx cozyclay --via`, use case and update status. (#466)
122
+
123
+ Tools
124
+
125
+ - A mocap research bench under `tools/bench/` and `tools/gt-render/`:
126
+ ground-truth part-colour renders with camera truths (#428), a GVHMR
127
+ extraction bench (#430), a scorer for pose, trajectory, endpoints, overlap
128
+ and contact (#431), 3D fit stages F0-F5 (#432), a cube-contact scenario
129
+ (#454), the observation-driven GVHMR tuning ladder G0-G5 (#462, #488,
130
+ #492) and the known-character tracker T1 (#500).
131
+
132
+ Housekeeping
133
+
134
+ - The camera-rail browser test waits for the delete toast, which exposed and
135
+ fixed a stale-toast dismissal bug (#470, #483); the lifecycle test waits
136
+ for the grandchild to disappear (#479).
137
+ - The owner's v2 editor design contract is adopted in the docs (#512).
138
+ - Repository traffic is archived to the `metrics` branch twice a week.
139
+
140
+ ## 1.10.0
141
+
142
+ Props and characters can now be hidden without being deleted, the Studio top
143
+ bar no longer collapses at 1100px when a live workspace handle is shown, and
144
+ the MorphGS mocap experiment gets its conversion scripts, a playback
145
+ regression test and a cluster setup script under `tools/morphgs/`.
146
+
147
+ Studio
148
+
149
+ - Hide or show a prop or a character from the hierarchy eye and the row
150
+ context menu. The row, selection and inspector stay; move handles appear
151
+ only while the object is on stage. A hidden parent or a hidden character
152
+ takes its children and carried props off the stage, out of collision and
153
+ off the ground while their own eye stays on. Hidden state survives
154
+ save/reload and Ctrl+Z. (#416, by @bjoernfal)
155
+ - MCP `update_object` and the Studio agent's object `update` op accept
156
+ `hidden`; `describe_scene` marks hidden props. The OTIO cut-list export
157
+ leaves hidden props out of the blocking metadata. (#416)
158
+ - The Inspector Parent select writes through the history store, so a
159
+ reparent no longer disappears on the next object edit. (#416)
160
+ - The `Live workspace <uuid>` handle in the top bar stays on one line and
161
+ truncates with an ellipsis instead of wrapping over Settings and Export at
162
+ 1100px; the responsive browser QA now asserts that top-bar chrome does
163
+ not overlap. (#321)
164
+
165
+ Tools
166
+
167
+ - `tools/morphgs/`: `fbx2morphgs.mjs` (x-bot FBX → MorphGS mesh + compressed
168
+ rig, drops the zero-length duplicate bones FBXLoader creates and appends the
169
+ required `fixed_joint` line), `morphgs-to-cskel27.mjs` (rot_params +
170
+ pred_joints → playback NPZ), `setup-on-cluster.sh`, `demo-gate.sh` and the
171
+ `--mode none` preprocess patch, with `test/verify-morphgs-exporter.mjs`
172
+ checking the exporter against MorphGS's own Animation.step. (#415)
173
+
3
174
  ## 1.9.0
4
175
 
5
176
  The Agent panel moves onto the open-source pi harness with six model
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
- import { basename } from "node:path";
3
+ import { basename, join } from "node:path";
4
+ import { pathToFileURL } from "node:url";
4
5
  import * as defaultAuth from "../codex-auth.mjs";
5
6
  import { publishLiveEndpoint, removeLiveEndpoint } from "../live-endpoint.mjs";
6
7
  import { createCodexClient } from "./codex-client.mjs";
@@ -230,10 +231,22 @@ function defaultClient(auth, requestContext) {
230
231
  const liveHubOwner = () => process.env.COZYCLAY_LIVE_OWNER
231
232
  || (basename(process.argv[1] ?? "") === "dev-full.mjs" ? "dev-full" : "cozyclay");
232
233
 
234
+ /** The two live-tool modules. A source checkout resolves them from the
235
+ * repository, where mcp/ has its own node_modules. The published package
236
+ * ships mcp/ without zod, three or ws, so the launcher passes an `mcpRuntime`
237
+ * whose ensure() installs the staged runtime (bin/mcp-runtime.mjs) once per
238
+ * version and the modules are imported from that root instead (#576). */
239
+ async function liveToolsModules(mcpRuntime) {
240
+ if (!mcpRuntime) return Promise.all([import("../../mcp/tool-handlers.mjs"), import("../../mcp/live-hub.mjs")]);
241
+ const root = await mcpRuntime.ensure();
242
+ const staged = (file) => pathToFileURL(join(root, "mcp", file)).href;
243
+ return Promise.all([import(staged("tool-handlers.mjs")), import(staged("live-hub.mjs"))]);
244
+ }
245
+
233
246
  /** Start the optional registry/live dependencies without making signed-out
234
247
  * startup depend on an MCP dependency install. Failures remain visible on use. */
235
- function liveToolsRuntime() {
236
- return Promise.all([import("../../mcp/tool-handlers.mjs"), import("../../mcp/live-hub.mjs")]).then(async ([registry, { startLiveHub }]) => {
248
+ function liveToolsRuntime(mcpRuntime) {
249
+ return liveToolsModules(mcpRuntime).then(async ([registry, { startLiveHub }]) => {
237
250
  const owner = liveHubOwner();
238
251
  const token = randomBytes(32).toString("hex");
239
252
  const liveHub = await startLiveHub(Number(process.env.COZYCLAY_LIVE_PORT ?? 5184), { token, owner });
@@ -258,13 +271,19 @@ function liveToolsRuntime() {
258
271
  liveHub.server?.once("close", () => removeLiveEndpoint(liveHub.port));
259
272
  }
260
273
  return { liveHub, handlers };
261
- }).catch((error) => ({ error }));
274
+ }).catch((error) => {
275
+ // The pane answers CAPABILITY_MISSING on use; the reason belongs on the
276
+ // launcher's terminal rather than nowhere.
277
+ console.error(`cozyclay: Studio live tools unavailable: ${error?.message ?? error}`);
278
+ for (const line of error?.lines ?? []) console.error(`cozyclay: ${line}`);
279
+ return { error };
280
+ });
262
281
  }
263
282
 
264
- export function createAgentHandler({ auth = defaultAuth, codex, models, codexBaseUrl, cliproxyBaseUrl, env, fauxProvider, handlers, liveHub, port, getBridgeOrigin, studioRuntime, clock = Date.now, setIntervalImpl = setInterval, clearIntervalImpl = clearInterval, sessionStore: injectedSessionStore } = {}) {
283
+ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBaseUrl, cliproxyBaseUrl, env, fauxProvider, handlers, liveHub, mcpRuntime, port, studioRuntime, clock = Date.now, setIntervalImpl = setInterval, clearIntervalImpl = clearInterval, sessionStore: injectedSessionStore } = {}) {
265
284
  const requestContext = new AsyncLocalStorage();
266
285
  codex ||= defaultClient(auth, requestContext);
267
- const runtime = handlers !== undefined || liveHub !== undefined ? Promise.resolve({ handlers: handlers ?? [], liveHub }) : liveToolsRuntime();
286
+ const runtime = handlers !== undefined || liveHub !== undefined ? Promise.resolve({ handlers: handlers ?? [], liveHub }) : liveToolsRuntime(mcpRuntime);
268
287
  const renderGuidance = async (environment) => {
269
288
  try {
270
289
  const { handlers: tools, liveHub: hub } = await runtime;
@@ -306,7 +325,6 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
306
325
  const sessionStore = injectedSessionStore ?? createSessionStore();
307
326
  const studioSessions = new Map();
308
327
  const studioEvents = new Map();
309
- let ownedStudioRuntime = studioRuntime || null;
310
328
  const studioOwnerTokens = new Map();
311
329
  const parseCookies = req => Object.fromEntries(String(req.headers.cookie || "").split(";").map(part => part.trim().split("=")).filter(([key, value]) => key && value).map(([key, value]) => [key, decodeURIComponent(value)]));
312
330
  const pruneStudioSessions = () => {
@@ -323,13 +341,6 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
323
341
  if (!owner || supplied !== owner) throw Object.assign(new Error("Studio session owner mismatch."), { code: "AUTH_REQUIRED" });
324
342
  return owner;
325
343
  };
326
- const studioRuntimeFor = async hub => {
327
- if (ownedStudioRuntime) return ownedStudioRuntime;
328
- const { createStudioMotionRuntime } = await import("./motion-runtime.mjs");
329
- if (!hub) return null;
330
- ownedStudioRuntime = createStudioMotionRuntime({ liveHub: hub, getBridgeOrigin, clock });
331
- return ownedStudioRuntime;
332
- };
333
344
  const emitStudioEvent = (turnId, event) => {
334
345
  const record = studioEvents.get(turnId) || { next: 0, events: [], listeners: new Set(), terminal: false };
335
346
  // Execution telemetry is advisory and is validated key-by-key in the
@@ -353,7 +364,6 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
353
364
  if (kind !== "signed_out" && kind !== "replaced") return;
354
365
  for (const session of sessions.values()) session.controller?.abort();
355
366
  for (const session of studioSessions.values()) session.controller?.abort();
356
- void ownedStudioRuntime?.dispose?.(); ownedStudioRuntime = studioRuntime || null;
357
367
  for (const runner of [...workflowRunners.values(), ...studioRunners.values()]) void runner.close?.();
358
368
  workflowRunners.clear(); studioRunners.clear();
359
369
  workflowModels = models;
@@ -401,32 +411,16 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
401
411
  // An owned retired id is still stale while another motion job is active:
402
412
  // this Stop cannot acknowledge the current turn's different job.
403
413
  if (value.jobId && session.activeJobId && value.jobId !== session.activeJobId) throw new StudioProtocolError("STALE_TARGET", "Stop does not target the active motion job.");
404
- // #379 / 16r: a job's id is only "acknowledged" by THIS turn's held motion
405
- // tool — session.activeJobId now stays set only while that job is genuinely
406
- // still in flight (or pending an explicit accept), and activeJobTurnId ties
407
- // it to the turn that admitted it. A retired job from an earlier turn must
408
- // never make a later, unrelated turn's Stop go quiet.
414
+ // Only this turn's held editor job can settle the tool quietly. A retired
415
+ // job from an earlier turn must never silence an unrelated turn's Stop.
409
416
  const acknowledged = Boolean(session.activeJobId) && session.activeJobTurnId === value.turnId;
410
- // The runtime is the only thing that knows whether the job was applied.
411
- // Forwarding its outcome keeps the panel from turning "I could not find
412
- // out" into "nothing was applied"; a discarded outcome reads as proof.
417
+ // The editor's outcome, not a sidecar abort, establishes application.
413
418
  let outcome = null;
414
- if (jobId && ownedStudioRuntime?.stop) outcome = await ownedStudioRuntime.stop(jobId);
419
+ if (jobId) outcome = await session.controlJob('job.cancel', jobId);
415
420
  if (jobId && session.activeJobId === jobId) { session.activeJobId = null; session.activeJobTurnId = null; }
416
- // #379 / 16q: `session.controller.signal` is the SAME signal wired into the
417
- // active turn's prompt context (`:520 signal: controller.signal`), which
418
- // pi's `withAbortSignal`/`awaitWithContext` races against every awaited
419
- // internal step — aborting it here interrupts the in-flight generate_motion
420
- // tool call's OWN completion machinery before it can settle, well before
421
- // pi's own graceful `lane.abort()` (below) ever runs. A job the runtime
422
- // actually acknowledged already resolved the held tool call with its real
423
- // outcome via `runtime.stop()` above; the only abort this stop still needs
424
- // is the graceful one that stops the model from being prompted again, so
425
- // `session.controller` must NOT be touched on that path (no other tool is
426
- // concurrently in flight while generate_motion holds the turn). A stop
427
- // for a job this turn never acknowledged (none active, or a retired one
428
- // from an earlier turn) never resolved anything for THIS turn and stays a
429
- // plain (loud) abort on both signals.
421
+ // Let the acknowledged job settle its held tool with that outcome before
422
+ // aborting the model lane. Aborting the shared signal would interrupt the
423
+ // tool first and lose the editor's proof. Without a held job, abort both.
430
424
  if (!acknowledged) session.controller?.abort();
431
425
  await session.modelSession?.abort?.("studio stop", acknowledged ? { quiet: true } : undefined);
432
426
  json(res, 200, { ok: true, status: jobId ? "stopped" : "detached", ...(outcome ? { outcome: { status: outcome.status ?? null, code: outcome.code ?? null, mutated: outcome.mutated ?? null } } : {}) }); return true;
@@ -440,7 +434,7 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
440
434
  studioOwner(req, value.sessionId, true);
441
435
  let persisted = null;
442
436
  try { persisted = sessionStore.read(value.sessionId); } catch { persisted = null; }
443
- session = { owner: studioOwnerTokens.get(value.sessionId), history: persisted?.history ?? [], persistedItems: persisted?.history?.length ?? 0, meta: persisted?.meta ?? null, turns: new Map(), controller: null, activeJobId: null, activeJobTurnId: null, motionJobIds: new Set(Array.isArray(persisted?.meta?.motionJobIds) ? persisted.meta.motionJobIds.filter(id => typeof id === "string") : []), generationPrompt: null, host: null, updatedAt: clock() };
437
+ session = { owner: studioOwnerTokens.get(value.sessionId), history: persisted?.history ?? [], persistedItems: persisted?.history?.length ?? 0, meta: persisted?.meta ?? null, turns: new Map(), controller: null, activeJobId: null, activeJobTurnId: null, motionJobIds: new Set(Array.isArray(persisted?.meta?.motionJobIds) ? persisted.meta.motionJobIds.filter(id => typeof id === "string") : []), host: null, updatedAt: clock() };
444
438
  studioSessions.set(value.sessionId, session);
445
439
  }
446
440
  session.updatedAt = clock();
@@ -468,33 +462,52 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
468
462
  const admission = {
469
463
  commandId: () => randomUUID(), host: studioIdentity(value.context.host), revision: value.context.revision.scene,
470
464
  refresh: async () => {
471
- const refreshed = studioRuntime?.readContext ? await studioRuntime.readContext(value.context.host) : await authoritativeStudioContext(value, hub);
465
+ // Read at the admitted host: a scene action may have opened another
466
+ // scene of this workspace during the turn (studio-tools adopts it).
467
+ const host = { ...value.context.host, ...admission.host };
468
+ const refreshed = studioRuntime?.readContext ? await studioRuntime.readContext(host) : await authoritativeStudioContext({ ...value, context: { ...value.context, host } }, hub);
472
469
  admission.revision = refreshed.revision.scene;
473
470
  },
474
471
  };
475
- const runtimeForJob = await studioRuntimeFor(hub);
476
- const tools = createStudioTools({ liveHub: hub, workspaceHandle: value.context.host.workspaceHandle, session: { signal: controller.signal, admission }, resolveImage: async (id, correlation) => hub.command("resolve_studio_image", { imageId: id, ...correlation }, value.context.host.workspaceHandle) });
477
- const motion = async args => {
478
- if (session.generationPrompt === value.text) throw new StudioProtocolError("AUTH_REQUIRED", "This generation request already has a retained result; start a new explicit request.");
479
- if (!runtimeForJob) throw new StudioProtocolError("CAPABILITY_MISSING", "Studio motion runtime is unavailable.");
480
- const character = value.context.entities.find(entity => entity.id === args.characterId && entity.kind === "character");
481
- if (!character) throw new StudioProtocolError("TARGET_NOT_READY", "The admitted character is unavailable.");
482
- const commandId = randomUUID(); const host = { ...value.context.host, workspaceHandle: value.context.host.workspaceHandle };
483
- const admissionResult = runtimeForJob.admit({ hostBinding: host, characterId: args.characterId, targetToken: character.token, turnId: value.turnId, commandId, authorization: { id: randomUUID(), generations: 1 }, source: args.source, repair: args.repair ?? "bounded" });
484
- session.motionJobIds.add(admissionResult.jobId); persistenceMeta.motionJobIds = [...session.motionJobIds]; session.activeJobId = admissionResult.jobId; session.activeJobTurnId = value.turnId; session.generationPrompt = value.text;
485
- const unsubscribe = runtimeForJob.subscribe(admissionResult.jobId, event => send({ ...event, sourceEventSeq: event.eventSeq }));
486
- // Subscription precedes start, including replay of the queued admission event.
487
- let outcome;
488
- try { outcome = await runtimeForJob.start(admissionResult.jobId); if (outcome?.ok && outcome.status === "installed") send({ type: "receipt", receipt: outcome }); return outcome; }
489
- finally {
490
- unsubscribe();
491
- // A job still awaiting an explicit accept (review_required) stays "active"
492
- // for the accept route's ownership check (:678); every other outcome —
493
- // installed, cancelled, proved-not-applied, failed, or a thrown error —
494
- // is terminal for this session and must not leak into a later turn's Stop.
495
- if (session.activeJobId === admissionResult.jobId && outcome?.status !== "review_required") { session.activeJobId = null; session.activeJobTurnId = null; }
472
+ // One motion generation per user message, whichever path starts it:
473
+ // generate_motion below and a run_action job action share this gate.
474
+ const generation = { used: false, failures: 0 };
475
+ // The editor's command index (authoritative context) declares each action's
476
+ // generation and hub timeout; no sidecar list of actions exists.
477
+ const controlJob = async (action, jobId) => {
478
+ await admission.refresh();
479
+ return hub.command('run_action', { name: 'run_action', args: { action, args: { jobId, ...(action === 'job.await' ? { timeoutMs: 300_000 } : {}) } },
480
+ commandId: randomUUID(), host: admission.host, expectedRevision: admission.revision }, value.context.host.workspaceHandle, { timeoutMs: 300_000 });
481
+ };
482
+ session.controlJob = controlJob;
483
+ const onJob = async started => {
484
+ const jobId = started.jobId;
485
+ session.motionJobIds.add(jobId);
486
+ persistenceMeta.motionJobIds = [...session.motionJobIds];
487
+ session.activeJobId = jobId; session.activeJobTurnId = value.turnId;
488
+ const cancel = () => { void controlJob('job.cancel', jobId).catch(error => send({ type: 'error', code: error.code ?? 'CANCEL_FAILED', message: error.message })); };
489
+ controller.signal.addEventListener('abort', cancel, { once: true });
490
+ if (controller.signal.aborted) cancel();
491
+ send({ type: 'job.state', jobId, commandId: started.commandId, state: 'generating', phase: 'generating' });
492
+ try {
493
+ const outcome = await controlJob('job.await', jobId);
494
+ send({ type: 'job.state', jobId, commandId: outcome.commandId, state: outcome.ok ? 'installed' : outcome.code === 'CANCELLED' ? 'cancelled' : 'failed', phase: 'complete' });
495
+ return outcome;
496
+ } finally {
497
+ controller.signal.removeEventListener('abort', cancel);
498
+ if (session.activeJobId === jobId) { session.activeJobId = null; session.activeJobTurnId = null; }
496
499
  }
497
500
  };
501
+ const tools = createStudioTools({ liveHub: hub, workspaceHandle: value.context.host.workspaceHandle, session: { signal: controller.signal, admission, generation, onJob, actionIndex: current?.actionIndex ?? [] }, resolveImage: async (id, correlation) => hub.command("resolve_studio_image", { imageId: id, ...correlation }, value.context.host.workspaceHandle) });
502
+ const motion = async args => {
503
+ // Private artifact IDs belonged to the retired sidecar runtime. Reuse
504
+ // retained editor takes through motion.loadVersion, not another installer.
505
+ if (args.source.kind !== 'generate') throw new StudioProtocolError('CAPABILITY_MISSING', 'Private motion artifacts are no longer retained. Use motion.loadVersion for an editor take.');
506
+ await tools.internal.invoke('inspect_studio', { scope: 'motion', ids: [args.characterId] });
507
+ const receipt = await tools.internal.invoke('generate_motion', args);
508
+ if (receipt.ok && receipt.status === 'completed') send({ type: 'receipt', receipt });
509
+ return receipt;
510
+ };
498
511
  const modelTools = tools.map(tool => ({
499
512
  ...tool,
500
513
  handler: async args => {
@@ -699,14 +712,6 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
699
712
  }
700
713
  return true;
701
714
  }
702
- if (path.startsWith("/agent/jobs/") && path.endsWith("/accept") && req.method === "POST") {
703
- if (!await hasAnyCredential()) { json(res, 401, { error: { code: "AUTH_REQUIRED", message: "Sign in or configure a provider key." } }); return true; }
704
- let value; try { value = await readBody(req); } catch { json(res, 400, { error: "invalid request" }); return true; }
705
- const jobId = decodeURIComponent(path.slice("/agent/jobs/".length, -"/accept".length)); const session = studioSessions.get(value?.sessionId);
706
- if (!session || session.owner !== parseCookies(req).studio_owner || value.surface !== "studio" || value.explicitUnverifiedAcceptance !== true || !session.turns.has(value.turnId) || session.activeJobId !== jobId) { json(res, 403, { error: { code: "AUTH_REQUIRED", message: "Only the owning Studio UI may accept this candidate." } }); return true; }
707
- try { const receipt = await ownedStudioRuntime.accept(jobId); const record = studioEvents.get(value.turnId); if (record) { emitStudioEvent(value.turnId, { type: "receipt", receipt }); emitStudioEvent(value.turnId, { type: "done" }); record.terminal = true; } session.activeJobId = null; session.activeJobTurnId = null; json(res, 200, { receipt }); } catch (error) { json(res, 409, { error: { code: error.code || "VERIFICATION_FAILED", message: error.message } }); }
708
- return true;
709
- }
710
715
  if (req.method !== "POST" || !["/agent/turn", "/agent/stop"].includes(path)) {
711
716
  json(res, 404, { error: "not found" }); return true;
712
717
  }
@@ -841,7 +846,6 @@ export function createAgentHandler({ auth = defaultAuth, codex, models, codexBas
841
846
  for (const runner of workflowRunners.values()) await runner.close?.();
842
847
  for (const runner of studioRunners.values()) await runner.close?.();
843
848
  workflowRunners.clear(); studioRunners.clear();
844
- if (ownedStudioRuntime?.dispose) await ownedStudioRuntime.dispose();
845
849
  studioSessions.clear(); studioEvents.clear(); studioOwnerTokens.clear();
846
850
  sessions.clear();
847
851
  const { liveHub: hub } = await runtime;
@@ -228,11 +228,18 @@ export function createAgentRunner({ models: suppliedModels, sessionStore, tools
228
228
  const messages = entries.filter((entry) => entry.type === "message").map((entry) => entry.message);
229
229
  if (messages.length <= state.persisted) return;
230
230
  const aborted = state.active?.abortRequested === true;
231
- const pending = messages.slice(state.persisted).filter((message) => !(aborted && message?.role === "assistant") && !(message?.role === "assistant" && message?.stopReason === "error"));
231
+ const newlyGenerated = messages.slice(state.persisted);
232
+ const failedAssistant = newlyGenerated.find((message) => message?.role === "assistant" && (aborted || message.stopReason === "error"));
233
+ const pending = newlyGenerated.filter((message) => !(aborted && message?.role === "assistant") && !(message?.role === "assistant" && message?.stopReason === "error"));
232
234
  state.persisted = messages.length;
233
- if (!pending.length) return;
234
235
  const input = state.lastInput || {};
235
- await sessionStore.append(sessionId, pending, { surface, ...(input.meta || {}) });
236
+ if (pending.length) await sessionStore.append(sessionId, pending, { surface, ...(input.meta || {}) });
237
+ if (failedAssistant && sessionStore.appendTurnError) {
238
+ const rawMessage = failedAssistant.errorMessage || "The model or live editor could not complete this turn.";
239
+ const message = aborted ? "The turn was aborted." : sanitizeUpstreamDetail(JSON.stringify({ message: rawMessage })) || rawMessage;
240
+ const code = aborted ? "aborted" : classifyProviderError(rawMessage);
241
+ await sessionStore.appendTurnError(sessionId, { code, message, at: new Date().toISOString() }, { surface, ...(input.meta || {}) });
242
+ }
236
243
  };
237
244
 
238
245
  // pi calls AgentTool.execute(toolCallId, params, signal, onUpdate) — the