cozyclay 2.1.1 → 3.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 (577) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +3 -3
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -2,8 +2,8 @@
2
2
  * Return the current content-driven length of the shared production clock.
3
3
  *
4
4
  * The active character is represented by the editing-buffer motion; inactive
5
- * characters keep their committed sessionMotion. Prompt block endFrame values
6
- * are already expressed as an exclusive timeline count, matching motion.frames.
5
+ * characters keep their committed sessionMotion (motion.frames is an exclusive
6
+ * timeline count).
7
7
  *
8
8
  * A zero result is intentional: an empty scene keeps its existing authored
9
9
  * duration instead of collapsing the pre-generation clock to one frame.
@@ -11,28 +11,16 @@
11
11
  export function timelineContentExtent(
12
12
  characters = [],
13
13
  activeCharacterId = null,
14
- activeMotion = null,
15
- promptClips = [],
14
+ activeMotion = null,
16
15
  extraFrames = 0,
17
16
  ) {
18
- const longestMotion = characters.reduce((max, entry) => {
17
+ const authoredExtent = characters.reduce((max, entry) => {
19
18
  const clip = entry.id === activeCharacterId ? activeMotion : entry.sessionMotion;
20
- const layerPromptEnd = entry.id === activeCharacterId
21
- ? 0
22
- : (entry.layer?.promptClips ?? []).reduce(
23
- (layerMax, clip) => Math.max(layerMax, Number.isFinite(clip?.endFrame) ? clip.endFrame : 0),
24
- 0,
25
- );
26
- return Math.max(max, Number.isFinite(clip?.frames) ? clip.frames : 0, layerPromptEnd);
19
+ return Math.max(max, Number.isFinite(clip?.frames) ? clip.frames : 0);
27
20
  }, 0);
28
- const promptEnd = promptClips.reduce(
29
- (max, clip) => Math.max(max, Number.isFinite(clip?.endFrame) ? clip.endFrame : 0),
30
- 0,
31
- );
32
- const authoredExtent = Math.max(longestMotion, promptEnd);
33
- // Ingested footage owns the clock only before a cast take or prompt
34
- // schedule exists. Once a 136-frame take is installed, an older 192-frame
35
- // source video must not recreate the frozen tail this helper prevents.
21
+ // Ingested footage owns the clock only before a cast take exists. Once a
22
+ // 136-frame take is installed, an older 192-frame source video must not
23
+ // recreate the frozen tail this helper prevents.
36
24
  return authoredExtent > 0
37
25
  ? authoredExtent
38
26
  : Number.isFinite(extraFrames) ? extraFrames : 0;
@@ -0,0 +1,17 @@
1
+ // The camera tutorial's set dressing: a short compiled walk (take v3) for the starter scene's
2
+ // character, so the Shot / Rail / Play steps have somebody moving through the set. Built from
3
+ // the character's own bind skeleton and placement; null for a body motion3 cannot drive.
4
+ import { skeletonFor } from "./motion3/rigs/index.js";
5
+ import { add, apply, ry } from "./motion3/compiler/math.js";
6
+
7
+ const TUTORIAL_WALK_LABEL = "A person walks forward and stops.";
8
+
9
+ export function tutorialWalkSpec(character) {
10
+ const sk = skeletonFor(character?.model);
11
+ if (!sk) return null;
12
+ const I = sk.index, P = sk.restWorldPos, ank = P[I.LeftFoot][1], yaw = character.rot ?? 0;
13
+ const at = (offset, y) => add([character.x ?? 0, character.y ?? 0, character.z ?? 0], apply(ry(yaw), [offset[0], y, offset[1]]));
14
+ return { schema: "cozyclay.motion-spec.v1", fps: 24, frames: 216, label: TUTORIAL_WALK_LABEL,
15
+ keys: [{ f: 0, set: { root: at([0, 0], P[I.Hips][1] - 0.01), yaw, foot_l: at([0.09, -0.03], ank), foot_r: at([-0.09, 0.03], ank), fyaw_l: yaw, fyaw_r: yaw } }],
16
+ generators: [{ type: "walk", f0: 6, steps: 8, heading: [yaw, yaw] }] };
17
+ }
package/src/ui-scrub.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * a pixel buy?". They live here so a Node test can prove them without a DOM.
8
8
  *
9
9
  * Sensitivity is a RATE over a fixed travel, the same rule the timeline's
10
- * curve editors follow (src/ardy/timeline.jsx DRAG_TRAVEL_PX): dragging the
10
+ * curve editors follow (src/motion/timeline.jsx DRAG_TRAVEL_PX): dragging the
11
11
  * hand across `SCRUB_TRAVEL_PX` sweeps one meaningful span of the field. The
12
12
  * old `pixels * step` mapping tied the feel to the field's precision, so a
13
13
  * step 0.05 field needed 100 px to move 5 units while a step 1 field bolted.
@@ -0,0 +1,12 @@
1
+ import { useMemo } from "react";
2
+ import { buildShotCameraTracks, castAimPoint, followSubjectTrack } from "./camera-direction.js";
3
+
4
+ /** The per-frame follow/rail camera of every shot (camera-direction.js), rebuilt
5
+ * when the shots, the subject's path or the cast a rail aims at change. */
6
+ export function useShotCameraTracks({ shots, subjectTrack, subjectId, fps, yawDeg, filmback, characters, objects, frameCount, activeId, motion }) {
7
+ return useMemo(() => {
8
+ if (!subjectTrack) return null;
9
+ const cast = { characters, objects, shots, frameCount, fps, clipOf: (entry) => (entry.id === activeId ? motion : entry.sessionMotion) };
10
+ return buildShotCameraTracks({ shots, subjectTrack: followSubjectTrack(subjectTrack, subjectId, cast), fps, yawDeg, filmback, aimAt: (id, frame) => castAimPoint(id, frame, cast) });
11
+ }, [shots, subjectTrack, subjectId, fps, yawDeg, filmback, characters, objects, frameCount, activeId, motion]);
12
+ }
@@ -0,0 +1,97 @@
1
+ // Onion-skin ghosts: translucent poses of the active character's take before and after the
2
+ // playhead, an editor overlay (ghost-set.js draws them on GIZMO_LAYER). This module holds the
3
+ // viewer setting (transient like the View menu: never saved, never undone) and the pure choices:
4
+ // which frames get a ghost and how each one looks.
5
+ import { useSyncExternalStore } from "react";
6
+ import { expandUse } from "./motion3/skills/index.js";
7
+ import { skeletonFor } from "./motion3/rigs/index.js";
8
+ import { motionScene } from "./motion3/scene.js";
9
+
10
+ /** off; step: every `step` frames; keys: the take spec's key frames nearest the playhead. */
11
+ export const GHOST_MODES = Object.freeze(["off", "step", "keys"]);
12
+ export const GHOST_COUNT = Object.freeze({ min: 1, max: 6 });
13
+ export const GHOST_STEP = Object.freeze({ min: 1, max: 24 });
14
+ const GHOST_DEFAULTS = Object.freeze({ mode: "off", count: 3, step: 4 });
15
+ /** Past poses cool, future poses warm. */
16
+ export const GHOST_COLORS = Object.freeze({ before: "#4d9cff", after: "#ff8a3d" });
17
+ const NEAR_OPACITY = 0.46, FAR_OPACITY = 0.14;
18
+
19
+ const clampInt = (value, { min, max }, fallback) => (Number.isFinite(value) ? Math.max(min, Math.min(max, Math.round(value))) : fallback);
20
+
21
+ /** A full, valid setting from a partial one (unknown modes keep the current mode). */
22
+ export function normalizeGhostSettings(patch = {}, base = GHOST_DEFAULTS) {
23
+ return Object.freeze({
24
+ mode: GHOST_MODES.includes(patch.mode) ? patch.mode : base.mode,
25
+ count: clampInt(patch.count, GHOST_COUNT, base.count),
26
+ step: clampInt(patch.step, GHOST_STEP, base.step),
27
+ });
28
+ }
29
+
30
+ let current = GHOST_DEFAULTS;
31
+ const listeners = new Set();
32
+ export const ghostSettings = () => current;
33
+ export function setGhostSettings(patch) {
34
+ const next = normalizeGhostSettings(patch, current);
35
+ if (next.mode === current.mode && next.count === current.count && next.step === current.step) return current;
36
+ current = next;
37
+ for (const listener of listeners) listener();
38
+ return current;
39
+ }
40
+ /** What a views contact sheet draws (verify_result ghosts): the viewport's setting while it is on, else 3 each side every 4 frames. */
41
+ export const sheetGhostSettings = () => (current.mode === "off" ? { ...GHOST_DEFAULTS, mode: "step" } : current);
42
+ const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); };
43
+ export const useGhostSettings = () => useSyncExternalStore(subscribe, ghostSettings, ghostSettings);
44
+
45
+ /** One sentence for receipts and tooltips. */
46
+ export function describeGhosts({ mode, count, step }) {
47
+ if (mode === "off") return "Ghosts off.";
48
+ return `Ghosts: ${count} before and ${count} after the playhead, ${mode === "keys" ? "at the spec's key frames" : `every ${step} frame${step === 1 ? "" : "s"}`}.`;
49
+ }
50
+
51
+ /**
52
+ * The frames that get a ghost around `frame` within 0..last: `count` on each side, every `step`
53
+ * frames, or the nearest of `keyFrames` (mode keys). rank 1 is the nearest; the playhead's own
54
+ * frame never gets one.
55
+ */
56
+ export function ghostFrames({ frame, last, mode, count, step, keyFrames = [] }) {
57
+ if (mode === "off" || !(last >= 0)) return [];
58
+ const at = Math.max(0, Math.min(last, Math.round(frame) || 0)), out = [];
59
+ const keys = mode === "keys" ? [...new Set(keyFrames.map(Math.round))].filter((f) => f >= 0 && f <= last).sort((x, y) => x - y) : null;
60
+ const before = keys ? keys.filter((f) => f < at).reverse() : null, after = keys ? keys.filter((f) => f > at) : null;
61
+ for (let rank = 1; rank <= count; rank++) {
62
+ const early = keys ? before[rank - 1] : at - rank * step, late = keys ? after[rank - 1] : at + rank * step;
63
+ if (early >= 0) out.push({ f: early, side: "before", rank });
64
+ if (late <= last) out.push({ f: late, side: "after", rank });
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /** Colour and opacity of a ghost: its side's hue, fading with its distance in time. */
70
+ export function ghostLook(side, rank, count) {
71
+ const t = count > 1 ? (rank - 1) / (count - 1) : 0;
72
+ return { color: GHOST_COLORS[side], opacity: +(NEAR_OPACITY + (FAR_OPACITY - NEAR_OPACITY) * t).toFixed(3) };
73
+ }
74
+
75
+ const keyCache = new WeakMap();
76
+ /**
77
+ * The key frames of an installed take v3: its spec's keys with the skills it uses expanded
78
+ * (expandUse, lenient), the held-object keys and each generator's start, ascending. A retimed
79
+ * take (fix.retime) is not remapped: these are the spec's frames. [] for anything else.
80
+ */
81
+ export function takeKeyFrames(take, objects = []) {
82
+ if (take?.schema !== "cozyclay.take.v3" || !take.spec) return [];
83
+ if (keyCache.has(take)) return keyCache.get(take);
84
+ let spec = take.spec;
85
+ try {
86
+ const sk = skeletonFor(take.rig);
87
+ if (sk && spec.use?.length) spec = expandUse(spec, { sk, scene: motionScene(objects), lenient: true }).spec;
88
+ } catch { spec = take.spec; }
89
+ const frames = [
90
+ ...(spec.keys ?? []).map((k) => k?.f),
91
+ ...Object.values(spec.objects ?? {}).flatMap((o) => (Array.isArray(o?.keys) ? o.keys.map((k) => k?.f) : [])),
92
+ ...(spec.generators ?? []).map((g) => g?.f0),
93
+ ].filter(Number.isFinite);
94
+ const keys = [...new Set(frames.map(Math.round))].sort((a, b) => a - b);
95
+ keyCache.set(take, keys);
96
+ return keys;
97
+ }
@@ -0,0 +1,49 @@
1
+ // The viewport's onion-skin ghosts (ghost-set.js) of the active character's take v3. They live on
2
+ // GIZMO_LAYER: the working views draw them, while the shot preview card, PlayView, captures,
3
+ // contact sheets and exports never do. Off by default; view.setGhosts and the View menu's Ghosts
4
+ // group turn them on (view-ghosts.js holds that setting).
5
+ import { useEffect, useMemo } from "react";
6
+ import { useFrame, useThree } from "@react-three/fiber";
7
+ import { GIZMO_LAYER } from "./dualview.jsx";
8
+ import { GhostSet } from "./ghost-set.js";
9
+ import { isTakeV3 } from "./motion3/playback.js";
10
+ import { ghostFrames, takeKeyFrames, useGhostSettings } from "./view-ghosts.js";
11
+
12
+ // Kit parts mount in the same commit as a take or record change, sometimes a frame later: the
13
+ // carried meshes are collected again on the next frames too.
14
+ const SOURCE_PASSES = 3;
15
+
16
+ /**
17
+ * rig: the active character's live rig; take: its installed motion; frame: the playhead;
18
+ * objects: the authored records; propTake: { frameCount, fps, objectTracks } as SetProps gets it.
19
+ */
20
+ export function ViewGhosts({ rig, take, frame, objects, propTake }) {
21
+ const settings = useGhostSettings();
22
+ const scene = useThree((state) => state.scene), gl = useThree((state) => state.gl), invalidate = useThree((state) => state.invalidate);
23
+ const set = useMemo(() => new GhostSet({ layer: GIZMO_LAYER }), []);
24
+ const on = settings.mode !== "off" && !!rig && isTakeV3(take);
25
+ const keyFrames = useMemo(() => (on && settings.mode === "keys" ? takeKeyFrames(take, objects) : []), [on, settings.mode, take, objects]);
26
+ const entries = useMemo(() => (on ? ghostFrames({ frame, last: take.frameCount - 1, ...settings, keyFrames }) : []),
27
+ [on, frame, take, settings, keyFrames]);
28
+ useEffect(() => {
29
+ if (!on) return undefined;
30
+ scene.add(set.object);
31
+ return () => { scene.remove(set.object); };
32
+ }, [on, scene, set]);
33
+ useEffect(() => () => set.dispose(), [set]);
34
+ // The canvas may render on demand: a new ghost frame set is a reason to draw.
35
+ useEffect(() => { invalidate(); }, [invalidate, on, entries]);
36
+ useEffect(() => {
37
+ // QA hook (test/qa-view-ghosts-browser.mjs): the live set and the renderer it draws with.
38
+ if (typeof window !== "undefined") window.__cozyclayGhosts = { set, gl, scene, entries };
39
+ }, [set, gl, scene, entries]);
40
+ useFrame(() => {
41
+ // A rig that left the stage (a hidden cast member) keeps no ghosts on it.
42
+ if (!set.show(on && !!rig.parent)) return;
43
+ set.setRig(rig);
44
+ set.syncSources(scene, objects, take, SOURCE_PASSES);
45
+ rig.parent.updateWorldMatrix(true, false);
46
+ set.update({ entries, take, placement: rig.parent.matrixWorld, propTake, objects, count: settings.count });
47
+ });
48
+ return null;
49
+ }
@@ -1,13 +1,6 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
- import { FiAlertTriangle, FiArrowUp, FiCheck, FiChevronRight, FiClock, FiDownload, FiFilm, FiImage, FiMoreHorizontal, FiPaperclip, FiPlus, FiRotateCw, FiX } from "react-icons/fi";
3
- import {
4
- ATTACHMENT_FAILED_NOTICE,
5
- ATTACHMENT_LIMIT_NOTICE,
6
- ATTACHMENT_TYPE_NOTICE,
7
- attachmentFilesFromTransfer,
8
- attachmentFromFile,
9
- transferCarriesFiles,
10
- } from "./attachment-image.js";
1
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { FiAlertTriangle, FiArrowUp, FiCheck, FiChevronRight, FiClock, FiDownload, FiImage, FiMoreHorizontal, FiPaperclip, FiPlus, FiRotateCw, FiX } from "react-icons/fi";
3
+ import { ATTACHMENT_FAILED_NOTICE, ATTACHMENT_LIMIT_NOTICE, ATTACHMENT_TYPE_NOTICE, attachmentFilesFromTransfer, attachmentFromFile, transferCarriesFiles } from "./attachment-image.js";
11
4
  import { isImeComposing } from "../ime.js";
12
5
  import { useStudioShell } from "../shell/studio-shell-context.js";
13
6
  import { ko } from "../locale.js";
@@ -31,10 +24,8 @@ import {
31
24
  clampPanelWidth,
32
25
  createAgentTransport,
33
26
  panelPresentation,
34
- providerEnvLabel,
35
27
  requestHostImageAction,
36
- resolveToolLabel,
37
- formatElapsed,
28
+ formatTurnClock,
38
29
  formatResetIn,
39
30
  readStoredPanelWidth,
40
31
  readStoredSupervisor,
@@ -47,6 +38,11 @@ import {
47
38
  supervisorFollowUp,
48
39
  } from "./agent-client.js";
49
40
  import "./agent-panel.css";
41
+ import { AgentProgressCard } from "./AgentProgressCard.jsx";
42
+ import { LoginPanel } from "./LoginPanel.jsx";
43
+ import { TurnSteps } from "./AgentTurnSteps.jsx";
44
+ import { ReplyText } from "./AgentReplyText.jsx";
45
+ import { stepLabel, turnLayout } from "./agent-turn.js";
50
46
 
51
47
  function relativeTime(value, now = Date.now()) {
52
48
  const at = Date.parse(value || "");
@@ -65,32 +61,6 @@ function StatusDot({ tone, title }) {
65
61
  return <span className={`agent-status-dot ${tone}`} title={title} aria-hidden="true" />;
66
62
  }
67
63
 
68
- function ToolCallCard({ call, presentation, onRetry }) {
69
- const tone = call.status === "running" ? "busy" : ["failed", "cancelled"].includes(call.status) ? "alert" : "ok";
70
- // The badge marks a call that edits the host surface, so it is drawn exactly
71
- // for the families that surface names.
72
- const hostTool = Object.hasOwn(presentation.toolLabels, call.name);
73
- const detail = call.error ? `error: ${call.error}` : JSON.stringify(call.result ?? call.args ?? {}, null, 2);
74
- return <div className={`agent-card agent-tool-card${call.status === "failed" ? " failed" : ""}`} data-tool-status={call.status} data-tool-name={call.name}>
75
- <details>
76
- <summary>
77
- <StatusDot tone={tone} title={call.status} />
78
- <span className="agent-tool-label">{resolveToolLabel(call, presentation.toolLabels)}</span>{hostTool && <span className="agent-tool-badge">{presentation.toolBadge}</span>}
79
- <span className="agent-tool-elapsed">{call.status === "running" ? "running…" : call.status === "cancelled" ? (call.result?.status === "not_applied" ? "not applied" : "result unknown") : formatElapsed(call.elapsedMs)}</span>
80
- <FiChevronRight size={12} aria-hidden="true" />
81
- </summary>
82
- <pre className="agent-tool-detail">{detail}</pre>
83
- </details>
84
- {call.failure && <div className="agent-error" role="alert">
85
- <span>{call.failure.message || ERROR_COPY[call.failure.code] || "The turn failed."}</span>
86
- <div className="agent-error-actions">
87
- <button type="button" className="agent-ghost-button agent-error-retry" onClick={onRetry}>Retry</button>
88
- <button type="button" className="agent-ghost-button agent-error-details" onClick={(event) => { const card = event.currentTarget.closest(".agent-tool-card"); const details = card?.querySelector("details"); if (details) details.open = !details.open; }}>Details</button>
89
- </div>
90
- </div>}
91
- </div>;
92
- }
93
-
94
64
  function ImageResultCard({ image, onUse, onUndo, onRegenerate, onOpen }) {
95
65
  const applying = image.apply?.status === "applying";
96
66
  const failed = image.apply?.status === "failed";
@@ -250,69 +220,15 @@ const RECOVERY_COPY = {
250
220
  new_intent: "Tell the agent what to do with the changed target.",
251
221
  reconcile: "The result is unknown; reconcile before changing this target.",
252
222
  sign_in: "Sign in again to continue.",
223
+ fix_key: "Log in again under Connect a model, then retry.",
253
224
  };
254
225
 
255
- function FailureCard({ failure, onRetry }) {
226
+ function FailureCard({ failure, onRetry, onFixKey }) {
256
227
  return <div className="agent-card agent-failure-card" data-failure-code={failure.code} role="alert">
257
228
  <div className="agent-failure-head"><FiAlertTriangle size={12} aria-hidden="true" /><span className="agent-failure-code">{failure.code}</span></div>
258
229
  <p className="agent-failure-message">{failure.message || "The command did not complete."}</p>
259
230
  <p className="agent-failure-recovery">{RECOVERY_COPY[failure.recovery?.action] || RECOVERY_COPY.none}{failure.preserved?.authoredState === "unchanged" ? " Nothing in the scene changed." : failure.preserved?.authoredState === "unknown" ? " Whether the scene changed is unknown." : ""}</p>
260
- {failure.recovery?.retryAllowed && <div className="agent-error-actions"><button type="button" className="agent-ghost-button agent-error-retry" onClick={onRetry}>Retry</button></div>}
261
- </div>;
262
- }
263
-
264
- // One provider's credential row. The key lives in this component and nowhere
265
- // else: it is typed into a password field, handed to the transport once and
266
- // dropped the moment the sidecar accepts it, so no store, no transcript and no
267
- // screenshot can ever carry it. The sidecar answers with the SOURCE of a key
268
- // (env / file), never with a key, which is what the row reports.
269
- function ProviderKeyRow({ provider, onSave, onRemove }) {
270
- const [draft, setDraft] = useState("");
271
- const [busy, setBusy] = useState(false);
272
- const [error, setError] = useState("");
273
- const fromEnv = provider.authSource === "env";
274
- const fromFile = provider.authSource === "file";
275
- const envLabel = `set by ${providerEnvLabel(provider.id)}`;
276
- const inputId = `agent-key-${provider.id}`;
277
- const run = async (action) => {
278
- setBusy(true);
279
- setError("");
280
- try {
281
- await action();
282
- // The key is gone from the page the instant it is stored.
283
- setDraft("");
284
- } catch (failure) {
285
- // The sidecar's own refusal, which never quotes key material.
286
- setError(failure?.message || "The sidecar refused that key.");
287
- } finally {
288
- setBusy(false);
289
- }
290
- };
291
- return <div className="agent-key-row" data-provider={provider.id} data-provider-source={provider.authSource || "none"} data-provider-signed-in={provider.signedIn ? "true" : "false"}>
292
- <div className="agent-key-head">
293
- <StatusDot tone={provider.signedIn ? "ok" : ""} title={provider.signedIn ? "key configured" : "no key"} />
294
- <label className="agent-key-label" htmlFor={inputId}>{provider.label}</label>
295
- {/* The head states WHERE the key lives in two words; the variable's own
296
- name belongs on the input it replaces, where there is room for it. */}
297
- <span className="agent-key-source">{fromEnv ? "environment" : fromFile ? "saved on this machine" : "no key"}</span>
298
- </div>
299
- <div className="agent-key-controls">
300
- <input
301
- id={inputId}
302
- className="agent-key-input"
303
- type="password"
304
- autoComplete="off"
305
- spellCheck="false"
306
- aria-label={`${provider.label} API key`}
307
- placeholder={fromEnv ? envLabel : provider.signedIn ? "Replace key" : "Paste API key"}
308
- disabled={fromEnv || busy}
309
- value={draft}
310
- onChange={(event) => { setDraft(event.target.value); setError(""); }}
311
- />
312
- <button type="button" className="agent-key-save" disabled={fromEnv || busy || !draft.trim()} onClick={() => run(() => onSave(provider.id, draft))}>Save</button>
313
- {fromFile && <button type="button" className="agent-ghost-button agent-key-remove" disabled={busy} onClick={() => run(() => onRemove(provider.id))}>Remove</button>}
314
- </div>
315
- {error && <p className="agent-key-error" role="alert">{error}</p>}
231
+ {failure.recovery?.retryAllowed && <div className="agent-error-actions">{failure.recovery.action === "fix_key" && onFixKey && <button type="button" className="agent-ghost-button agent-error-keys" onClick={onFixKey}>Log in again</button>}<button type="button" className="agent-ghost-button agent-error-retry" onClick={onRetry}>Retry</button></div>}
316
232
  </div>;
317
233
  }
318
234
 
@@ -336,7 +252,6 @@ export default function AgentPanel({
336
252
  surface = "workflow",
337
253
  buildContext = null,
338
254
  onImageAction = null,
339
- onI2vAction = null,
340
255
  onReceipt = null,
341
256
  }) {
342
257
  // What this host shows around the shared conversation: labels, chips, the
@@ -405,10 +320,8 @@ export default function AgentPanel({
405
320
  const [lightbox, setLightbox] = useState(null);
406
321
  const [overlay, setOverlay] = useState(() => !embedded && (globalThis.innerWidth || 1440) < AGENT_PANEL_OVERLAY_BREAKPOINT);
407
322
  const [historyOpen, setHistoryOpen] = useState(false);
408
- // Provider credentials, read from the sidecar only while the section is open.
323
+ // Connect a model (pi's login list, LoginPanel.jsx) reads its own list from the sidecar while it is open.
409
324
  const [keysOpen, setKeysOpen] = useState(false);
410
- const [providers, setProviders] = useState([]);
411
- const [providersState, setProvidersState] = useState("loading");
412
325
  const [historySessions, setHistorySessions] = useState([]);
413
326
  const [restoreNotice, setRestoreNotice] = useState("");
414
327
  // Why the last steer did not reach the running turn. It reads in the same
@@ -427,8 +340,6 @@ export default function AgentPanel({
427
340
  buildContextRef.current = buildContext;
428
341
  const imageActionRef = useRef(onImageAction);
429
342
  imageActionRef.current = onImageAction;
430
- const i2vActionRef = useRef(onI2vAction);
431
- i2vActionRef.current = onI2vAction;
432
343
  const receiptRef = useRef(onReceipt);
433
344
  receiptRef.current = onReceipt;
434
345
 
@@ -479,10 +390,15 @@ export default function AgentPanel({
479
390
  // is live and holding the turn's outcome for a few seconds after it ends.
480
391
  const [activityNow, setActivityNow] = useState(() => Date.now());
481
392
  const activity = model
482
- ? describeActivity(chat, { now: activityNow, toolLabel: (call) => resolveToolLabel(call, presentation.toolLabels) })
393
+ ? describeActivity(chat, { now: activityNow, toolLabel: (call) => stepLabel(call, presentation.toolLabels) })
483
394
  : modelsState === "failed"
484
395
  ? { phase: "no-model", kind: "terminal", tone: "alert", text: "No model available — the agent service did not answer with one", ticking: false }
485
396
  : { phase: "models", kind: "live", tone: "busy", text: "Loading models…", ticking: false };
397
+ // The transcript grouped per turn, and the motion progress card (above the latest turn's steps, so the live
398
+ // step stays at the bottom where the transcript scrolls) limited to that turn's drafts.
399
+ const layout = useMemo(() => turnLayout(items, { streaming }), [items, streaming]);
400
+ const turnSince = streaming ? chat.turnStartedAt : chat.lastTurn ? chat.lastTurn.endedAt - chat.lastTurn.durationMs : null;
401
+ const progressCard = <AgentProgressCard items={items} since={turnSince} live={streaming} />;
486
402
  useEffect(() => {
487
403
  if (!activity.ticking) return;
488
404
  const timer = setInterval(() => setActivityNow(Date.now()), 500);
@@ -510,21 +426,6 @@ export default function AgentPanel({
510
426
  return null;
511
427
  }
512
428
  }, [sessionState, transport]);
513
- // The sidecar owns the credentials; this reads which providers have one and
514
- // where it came from. The list is read when the keys section opens and again
515
- // after every write — a key that just landed (or left) changes which models
516
- // the composer may offer, so the model list is re-read with it.
517
- const readProviders = useCallback(async () => {
518
- try {
519
- const list = await transport.providers?.();
520
- if (!Array.isArray(list)) throw new Error("no provider list");
521
- setProviders(list);
522
- setProvidersState("ready");
523
- } catch {
524
- setProviders([]);
525
- setProvidersState("failed");
526
- }
527
- }, [transport]);
528
429
  // Everything an authentication change moves, read back once: WHO the session
529
430
  // is (readiness is `signedIn || providersConfigured > 0`), which providers
530
431
  // hold a credential, and WHICH models this session may therefore send to. A
@@ -535,13 +436,11 @@ export default function AgentPanel({
535
436
  // into a composer without reloading the page, and what takes it away again
536
437
  // when the last way in goes. No timer is involved.
537
438
  const refreshProviderState = useCallback(async () => {
538
- const session = readAccount();
539
- await readProviders();
540
- await session;
439
+ await readAccount();
541
440
  try {
542
441
  applyModelList(await transport.models());
543
442
  } catch { /* the composer keeps the list it was last advertised */ }
544
- }, [applyModelList, readAccount, readProviders, transport]);
443
+ }, [applyModelList, readAccount, transport]);
545
444
 
546
445
  useEffect(() => {
547
446
  let cancelled = false;
@@ -798,20 +697,13 @@ export default function AgentPanel({
798
697
  // refresh above, because a key changes the session, not just a row.
799
698
  const toggleProviderKeys = useCallback(() => {
800
699
  setMenuOpen(false);
801
- const next = !keysOpen;
802
- setKeysOpen(next);
803
- if (!next) return;
804
- setProvidersState("loading");
805
- readProviders();
806
- }, [keysOpen, readProviders]);
807
- const saveProviderKey = useCallback(async (id, key) => {
808
- await transport.setProviderKey(id, key);
809
- await refreshProviderState();
810
- }, [refreshProviderState, transport]);
811
- const removeProviderKey = useCallback(async (id) => {
812
- await transport.removeProviderKey(id);
813
- await refreshProviderState();
814
- }, [refreshProviderState, transport]);
700
+ setKeysOpen(!keysOpen);
701
+ }, [keysOpen]);
702
+ // Opens (never toggles) Connect a model, pi's login list: the signed-out card and a refused key both lead here.
703
+ const openProviderKeys = useCallback(() => {
704
+ setMenuOpen(false);
705
+ setKeysOpen(true);
706
+ }, []);
815
707
 
816
708
  // Clearing the transcript also retires the session, so what the author sees
817
709
  // and what the model remembers cannot diverge.
@@ -927,8 +819,8 @@ export default function AgentPanel({
927
819
  {resetLabel && <span className="agent-account-reset">resets in {resetLabel}</span>}
928
820
  </div>}
929
821
  <button type="button" role="menuitem" onClick={clearContext}>Clear context</button>
930
- <button type="button" role="menuitem" className="agent-menu-keys" aria-expanded={keysOpen} onClick={toggleProviderKeys}>Provider keys…</button>
931
- <button type="button" role="menuitem" onClick={signOut}>Sign out</button>
822
+ <button type="button" role="menuitem" className="agent-menu-keys" aria-expanded={keysOpen} onClick={toggleProviderKeys}>Connect a model…</button>
823
+ {account?.signedIn && <button type="button" role="menuitem" onClick={signOut}>Sign out</button>}
932
824
  </div>}
933
825
  </span>
934
826
  {!embedded && <button type="button" className="agent-icon-button agent-collapse" onClick={() => setCollapsed(true)} aria-label="Collapse agent panel" title="Collapse agent panel (Cmd/Ctrl+B)"><FiChevronRight size={13} /></button>}
@@ -939,28 +831,21 @@ export default function AgentPanel({
939
831
 
940
832
  {/* An inline section, not a modal: the panel stays the one place the
941
833
  conversation and the credentials it runs on are managed. */}
942
- {keysOpen && <section className="agent-keys" aria-label="Provider keys" data-agent-keys="true">
943
- <div className="agent-keys-head">
944
- <h3 className="agent-keys-title">Provider keys</h3>
945
- <button type="button" className="agent-icon-button agent-keys-close" aria-label="Close provider keys" onClick={() => setKeysOpen(false)}><FiX size={11} aria-hidden="true" /></button>
946
- </div>
947
- <p className="agent-keys-hint">Keys are stored by the local CozyClay service and are never shown again.</p>
948
- {providersState === "loading" && <p className="agent-keys-note">Loading providers…</p>}
949
- {providersState === "failed" && <p className="agent-keys-note alert" role="alert">The provider list is unavailable.</p>}
950
- {providers.filter((entry) => entry.id !== "openai-codex").map((entry) => <ProviderKeyRow key={entry.id} provider={entry} onSave={saveProviderKey} onRemove={removeProviderKey} />)}
951
- </section>}
834
+ {keysOpen && <LoginPanel onClose={() => setKeysOpen(false)} onChatGPT={signIn} onChanged={refreshProviderState} />}
952
835
 
953
836
  <div className="agent-transcript" ref={transcriptRef} aria-live="polite" aria-label="Conversation" data-agent-transcript="true">
954
837
  {authState === "signed-out" && <div className="agent-state-card" data-agent-card="signed-out">
955
- <h3>Sign in with ChatGPT</h3>
956
- <p>The agent runs against your ChatGPT plan. Sign-in happens in your browser — no token is ever stored in this page.</p>
838
+ <h3>Connect a model</h3>
839
+ <p>Sign in with ChatGPT, or log in to another provider with a subscription, an API key, or your own proxy server. Sign-in happens in your browser — no token is ever stored in this page.</p>
957
840
  <button type="button" className="agent-primary-button agent-signin" onClick={signIn}>Sign in with ChatGPT</button>
841
+ <button type="button" className="agent-ghost-button agent-use-key" onClick={openProviderKeys}>Log in with another provider</button>
958
842
  </div>}
959
843
 
960
844
  {authState === "signing-in" && <div className="agent-state-card" data-agent-card="signing-in">
961
845
  <span className="agent-spinner" aria-hidden="true" />
962
846
  <h3>Waiting for your browser…</h3>
963
- <p>Finish the ChatGPT sign-in in the tab that just opened. This panel picks up the session when you come back.</p>
847
+ <p>Finish the ChatGPT sign-in in the browser page that just opened. This panel picks up the session when you come back.</p>
848
+ {account?.authorizeUrl && <a className="agent-ghost-button agent-signin-link" href={account.authorizeUrl} target="_blank" rel="noopener noreferrer">Open the sign-in page again</a>}
964
849
  </div>}
965
850
 
966
851
  {authState === "no-entitlement" && <div className="agent-state-card" data-agent-card="no-entitlement">
@@ -977,22 +862,28 @@ export default function AgentPanel({
977
862
  </div>
978
863
  </div>}
979
864
 
980
- {items.map((item) => {
865
+ {/* One step list per turn (AgentTurnSteps.jsx); the motion progress card sits just above the latest one. */}
866
+ {layout.map((entry) => {
867
+ if (entry.type === "steps") return <Fragment key={entry.id}>
868
+ {entry.last && progressCard}
869
+ <div className="agent-row"><TurnSteps block={entry} now={activityNow} toolLabels={presentation.toolLabels} formatClock={formatTurnClock} onRetry={() => runTurn(chat.lastPrompt)} /></div>
870
+ </Fragment>;
871
+ const { item } = entry;
981
872
  if (item.kind === "user") return <div className="agent-row user" key={item.id}>
982
873
  {item.attachments?.length > 0 && <div className="agent-bubble-attachments">
983
874
  {item.attachments.map((attachment, index) => <img key={`${item.id}-${index}`} src={attachment.dataUrl} alt={attachment.name || "Attached image"} onClick={() => setLightbox({ dataUrl: attachment.dataUrl, prompt: attachment.name })} />)}
984
875
  </div>}
985
876
  <div className="agent-bubble">{item.text}</div>
986
877
  </div>;
987
- if (item.kind === "assistant") return <div className="agent-row assistant" key={item.id}><div className="agent-assistant-text">{item.text}{streaming && <span className="agent-caret">▌</span>}</div></div>;
988
- if (item.kind === "tool") return <div className="agent-row" key={item.id}><ToolCallCard call={item} presentation={presentation} onRetry={() => runTurn(chat.lastPrompt)} /></div>;
878
+ if (item.kind === "assistant") return <div className="agent-row assistant" key={item.id}><div className="agent-assistant-text"><ReplyText text={item.text} tail={streaming && <span className="agent-caret">▌</span>} /></div></div>;
989
879
  if (item.kind === "job") return <div className="agent-row" key={item.id}><JobCard job={item} onStop={stopTurn} onAccept={(job) => store.acceptJob(job.jobId)} /></div>;
990
880
  if (item.kind === "receipt") return <div className="agent-row" key={item.id}><ReceiptCard item={item} /></div>;
991
- if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt)} /></div>;
881
+ if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt)} onFixKey={openProviderKeys} /></div>;
992
882
  if (item.kind === "supervisor") return <div className="agent-row" key={item.id}><SupervisorCard note={item} onSend={(text) => { store.setDraft(text); runTurn(text); }} /></div>;
993
883
  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>;
994
884
  })}
995
885
 
886
+ {!layout.some((entry) => entry.type === "steps" && entry.last) && progressCard}
996
887
  {rateLimit && <PausedCard resetAt={rateLimit.resetAt} onRetry={() => { store.clearRateLimit(); runTurn(chat.lastPrompt); }} onSwitchModel={switchModel} />}
997
888
  </div>
998
889
 
@@ -1001,6 +892,7 @@ export default function AgentPanel({
1001
892
  {authenticated && <div className="agent-activity" data-agent-activity={activity.phase} data-agent-activity-kind={activity.kind} role="status" aria-live="polite">
1002
893
  <span className={`agent-activity-indicator ${activity.tone}`} aria-hidden="true" />
1003
894
  <span className="agent-activity-text">{activity.text}</span>
895
+ {activity.detail && <span className="agent-activity-detail">{activity.detail}</span>}
1004
896
  </div>}
1005
897
 
1006
898
  {authenticated && <div
@@ -1062,17 +954,6 @@ export default function AgentPanel({
1062
954
  </button>
1063
955
  )}
1064
956
  <span className="agent-composer-spacer" aria-hidden="true" />
1065
- {surface === "studio" && i2vActionRef.current && <button
1066
- type="button"
1067
- className="agent-attach-chip agent-i2v-action"
1068
- data-testid="i2v-agent-action"
1069
- aria-label="Generate motion"
1070
- title="Generate motion from this message"
1071
- disabled={composerDisabled || !draft.trim() || streaming}
1072
- onClick={() => { const instruction = draft.trim(); store.setDraft(""); i2vActionRef.current?.(instruction); }}
1073
- >
1074
- <FiFilm size={13} aria-hidden="true" />
1075
- </button>}
1076
957
  {/* While a turn runs, Stop is still the way out of it; on a surface
1077
958
  that can be steered, Send becomes the way INTO it. */}
1078
959
  {streaming