@dailies-studio/shot-editor 0.1.0 → 0.2.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 (432) hide show
  1. package/dist/.vite/manifest.json +382 -0
  2. package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
  3. package/dist/assets/AgentPanel-DJEphah0.css +1 -0
  4. package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
  5. package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
  6. package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
  7. package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
  8. package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
  9. package/dist/assets/CurvePane-BjFVabKX.css +1 -0
  10. package/dist/assets/CurvePane-R22WzllJ.js +1 -0
  11. package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
  12. package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
  13. package/dist/assets/FontPicker-v0IOIArM.css +1 -0
  14. package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
  15. package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
  16. package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
  17. package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
  18. package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
  19. package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
  20. package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
  21. package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
  22. package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
  23. package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
  24. package/dist/assets/SoundMode-BmcZru3i.js +3 -0
  25. package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
  26. package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
  27. package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
  28. package/dist/assets/board-B88EPuLO.css +1 -0
  29. package/dist/assets/board-CL-he7SL.js +3 -0
  30. package/dist/assets/components-0oOSPN0l.js +1 -0
  31. package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
  32. package/dist/assets/googleFonts-BNon6M7X.js +1 -0
  33. package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
  34. package/dist/assets/index-4F5NQmI8.css +1 -0
  35. package/dist/assets/index-BGHQGV5z.js +117 -0
  36. package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  37. package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  38. package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  39. package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  40. package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  41. package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  42. package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  43. package/dist/assets/libraryModel-CpIwfFha.js +1 -0
  44. package/dist/assets/lucide-6iK64tTB.js +9319 -0
  45. package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
  46. package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
  47. package/dist/assets/webmcp-BsB9gxWC.js +2 -0
  48. package/dist/index.html +34 -3
  49. package/package.json +12 -9
  50. package/src/addFamilies.ts +66 -0
  51. package/src/api.ts +1631 -55
  52. package/src/app/App.tsx +977 -198
  53. package/src/app/lazyPanel.tsx +137 -0
  54. package/src/app/lazySurfaces.tsx +203 -0
  55. package/src/audio/index.tsx +41 -10
  56. package/src/audio/meter.ts +485 -0
  57. package/src/audio/meterMath.ts +273 -0
  58. package/src/audio/meterStore.ts +155 -0
  59. package/src/audio/offlineMix.ts +289 -0
  60. package/src/audio/preview.ts +928 -118
  61. package/src/bridge.ts +15 -5
  62. package/src/bridgeTypes.ts +1611 -36
  63. package/src/channels.ts +17 -32
  64. package/src/fonts/googleFonts.ts +4 -0
  65. package/src/hostApi.ts +174 -0
  66. package/src/icons.tsx +770 -0
  67. package/src/layerWords.ts +61 -0
  68. package/src/main.tsx +56 -4
  69. package/src/panels/ResizeHandle.tsx +33 -43
  70. package/src/panels/agent/AgentDots.tsx +36 -0
  71. package/src/panels/agent/AgentPanel.tsx +119 -43
  72. package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
  73. package/src/panels/agent/agent.css +113 -97
  74. package/src/panels/agent/agentGate.ts +35 -0
  75. package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
  76. package/src/panels/code/CodeStudio.tsx +373 -0
  77. package/src/panels/code/FileTree.tsx +265 -0
  78. package/src/panels/code/codeBufferGuard.ts +52 -0
  79. package/src/panels/code/codeParts.tsx +321 -0
  80. package/src/panels/comments/CommentComposer.tsx +239 -0
  81. package/src/panels/comments/CommentThread.tsx +346 -0
  82. package/src/panels/comments/CommentsPanel.tsx +336 -0
  83. package/src/panels/comments/InboxButton.tsx +131 -0
  84. package/src/panels/comments/MentionMenu.tsx +98 -0
  85. package/src/panels/comments/parts.tsx +171 -0
  86. package/src/panels/export/ExportDialog.tsx +919 -96
  87. package/src/panels/export/ExportScrim.tsx +57 -0
  88. package/src/panels/files/AssetsTab.tsx +656 -0
  89. package/src/panels/files/FilePanel.tsx +1466 -216
  90. package/src/panels/files/LayerMarks.tsx +40 -0
  91. package/src/panels/files/LeftColumn.tsx +37 -0
  92. package/src/panels/files/McpSection.tsx +690 -47
  93. package/src/panels/files/MissingAssetsNotice.tsx +60 -0
  94. package/src/panels/files/assetDrag.ts +79 -0
  95. package/src/panels/files/resourceParts.tsx +255 -0
  96. package/src/panels/fonts/FontPicker.tsx +762 -0
  97. package/src/panels/fonts/faceCache.ts +207 -0
  98. package/src/panels/fonts/fontPicker.css +271 -0
  99. package/src/panels/fonts/pickerModel.ts +265 -0
  100. package/src/panels/fonts/pickerRequest.ts +46 -0
  101. package/src/panels/header/AccountAvatar.tsx +76 -0
  102. package/src/panels/header/AgentChip.tsx +77 -0
  103. package/src/panels/header/AppMenu.tsx +213 -0
  104. package/src/panels/header/Header.tsx +564 -84
  105. package/src/panels/header/HistoryControls.tsx +31 -5
  106. package/src/panels/header/PresenceStack.tsx +329 -0
  107. package/src/panels/header/ProjectTitle.tsx +232 -0
  108. package/src/panels/header/ShareButton.tsx +59 -0
  109. package/src/panels/header/StatusBadges.tsx +358 -27
  110. package/src/panels/header/SyncDeletedDialog.tsx +88 -0
  111. package/src/panels/header/ZoomControls.tsx +46 -15
  112. package/src/panels/header/agentHandoff.ts +52 -0
  113. package/src/panels/header/leave.ts +26 -0
  114. package/src/panels/help/HelpSection.tsx +189 -163
  115. package/src/panels/insert/InsertDrawer.tsx +723 -0
  116. package/src/panels/insert/iconSet.ts +109 -0
  117. package/src/panels/inspector/ExposeDot.tsx +117 -0
  118. package/src/panels/inspector/FramingRows.tsx +314 -44
  119. package/src/panels/inspector/InspectorFooter.tsx +91 -0
  120. package/src/panels/inspector/InspectorHead.tsx +425 -0
  121. package/src/panels/inspector/InspectorPanel.tsx +408 -124
  122. package/src/panels/inspector/KeyframeDot.tsx +373 -0
  123. package/src/panels/inspector/KeyframeSection.tsx +257 -0
  124. package/src/panels/inspector/MarkerSection.tsx +55 -11
  125. package/src/panels/inspector/ParamRow.tsx +84 -53
  126. package/src/panels/inspector/RightColumn.tsx +152 -0
  127. package/src/panels/inspector/TimeSection.tsx +866 -0
  128. package/src/panels/inspector/animate/EaseControl.tsx +244 -0
  129. package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
  130. package/src/panels/inspector/animate/ShotSection.tsx +562 -48
  131. package/src/panels/inspector/design/AlignRow.tsx +95 -31
  132. package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
  133. package/src/panels/inspector/design/ClonerSection.tsx +323 -62
  134. package/src/panels/inspector/design/ComponentSection.tsx +50 -43
  135. package/src/panels/inspector/design/CornerSection.tsx +45 -14
  136. package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
  137. package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
  138. package/src/panels/inspector/design/DeviceSection.tsx +210 -0
  139. package/src/panels/inspector/design/FillSection.tsx +83 -51
  140. package/src/panels/inspector/design/FontField.tsx +196 -0
  141. package/src/panels/inspector/design/HtmlSection.tsx +23 -12
  142. package/src/panels/inspector/design/InstanceSection.tsx +373 -154
  143. package/src/panels/inspector/design/MaskSection.tsx +84 -0
  144. package/src/panels/inspector/design/MaterialSection.tsx +177 -0
  145. package/src/panels/inspector/design/MediaSection.tsx +273 -120
  146. package/src/panels/inspector/design/MeshSection.tsx +910 -82
  147. package/src/panels/inspector/design/PaintRows.tsx +550 -0
  148. package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
  149. package/src/panels/inspector/design/ShadowSection.tsx +221 -48
  150. package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
  151. package/src/panels/inspector/design/StrokeSection.tsx +228 -43
  152. package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
  153. package/src/panels/inspector/design/TransformSection.tsx +244 -50
  154. package/src/panels/inspector/design/TypographySection.tsx +679 -110
  155. package/src/panels/inspector/layout.ts +295 -0
  156. package/src/panels/inspector/propValue.ts +199 -0
  157. package/src/panels/inspector/scene/CameraSection.tsx +232 -22
  158. package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
  159. package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
  160. package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
  161. package/src/panels/inspector/scene/SceneSection.tsx +219 -44
  162. package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
  163. package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
  164. package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
  165. package/src/panels/inspector/timeModel.ts +190 -0
  166. package/src/panels/inspector/useDocWrite.ts +438 -146
  167. package/src/panels/library/LibraryBrowser.tsx +435 -0
  168. package/src/panels/library/LibraryGrid.tsx +190 -0
  169. package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
  170. package/src/panels/library/LibraryRail.tsx +84 -0
  171. package/src/panels/library/LibrarySoundPanel.tsx +293 -0
  172. package/src/panels/library/SoundHistory.tsx +245 -0
  173. package/src/panels/library/StockSounds.tsx +390 -0
  174. package/src/panels/library/libraryModel.ts +147 -0
  175. package/src/panels/library/stockSearch.ts +77 -0
  176. package/src/panels/maskMarks.tsx +117 -0
  177. package/src/panels/material/MaterialCommentNode.tsx +54 -0
  178. package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
  179. package/src/panels/material/MaterialGraphMenu.tsx +45 -0
  180. package/src/panels/material/MaterialGroupNode.tsx +56 -0
  181. package/src/panels/material/MaterialLibrary.tsx +253 -0
  182. package/src/panels/material/MaterialNode.tsx +453 -0
  183. package/src/panels/material/MaterialNodePalette.tsx +349 -0
  184. package/src/panels/material/MaterialParameterControls.tsx +525 -0
  185. package/src/panels/material/MaterialRerouteNode.tsx +42 -0
  186. package/src/panels/material/MaterialWorkspace.tsx +501 -0
  187. package/src/panels/material/NodePreview.tsx +70 -0
  188. package/src/panels/material/flowModel.ts +441 -0
  189. package/src/panels/material/parameterValues.ts +231 -0
  190. package/src/panels/material/previewScheduler.ts +235 -0
  191. package/src/panels/material/reactFlow.css +6 -0
  192. package/src/panels/material/socketGlyph.ts +85 -0
  193. package/src/panels/material/viewportMemory.ts +58 -0
  194. package/src/panels/preferences/PreferencesDialog.tsx +215 -0
  195. package/src/panels/share/ShareComponentDialog.tsx +247 -0
  196. package/src/panels/stage/StageMenu.tsx +35 -5
  197. package/src/panels/stage/StagePanel.tsx +338 -23
  198. package/src/panels/stage/cellSlots.tsx +70 -0
  199. package/src/panels/storyboard/BoardCanvas.tsx +739 -0
  200. package/src/panels/storyboard/BoardCursors.tsx +158 -0
  201. package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
  202. package/src/panels/storyboard/BoardPlan.tsx +42 -0
  203. package/src/panels/storyboard/ComponentSheet.tsx +187 -0
  204. package/src/panels/storyboard/ComponentStage.tsx +235 -0
  205. package/src/panels/storyboard/MiniSketch.tsx +173 -0
  206. package/src/panels/storyboard/PartPicker.tsx +64 -0
  207. package/src/panels/storyboard/PlanPanel.tsx +617 -0
  208. package/src/panels/storyboard/ReferencePreview.tsx +217 -0
  209. package/src/panels/storyboard/ShotBrief.tsx +360 -0
  210. package/src/panels/storyboard/ShotCard.tsx +251 -0
  211. package/src/panels/storyboard/StagePreview.tsx +348 -0
  212. package/src/panels/storyboard/StillThumbs.tsx +162 -0
  213. package/src/panels/storyboard/StoryboardView.tsx +79 -0
  214. package/src/panels/storyboard/board.css +1065 -0
  215. package/src/panels/storyboard/board.ts +12 -0
  216. package/src/panels/storyboard/boardClipboard.ts +111 -0
  217. package/src/panels/storyboard/boardOrder.ts +68 -0
  218. package/src/panels/storyboard/boardRows.tsx +425 -0
  219. package/src/panels/storyboard/liveCap.ts +90 -0
  220. package/src/panels/storyboard/plan.css +295 -0
  221. package/src/panels/storyboard/posterCache.ts +133 -0
  222. package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
  223. package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
  224. package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
  225. package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
  226. package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
  227. package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
  228. package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
  229. package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
  230. package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
  231. package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
  232. package/src/panels/storyboard/shotboard/sketch.css +1392 -0
  233. package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
  234. package/src/panels/storyboard/storyboardDrop.ts +164 -0
  235. package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
  236. package/src/panels/studio/ComponentSidePanel.tsx +355 -0
  237. package/src/panels/studio/ComponentStudio.tsx +243 -0
  238. package/src/panels/studio/ComponentsHome.tsx +318 -0
  239. package/src/panels/studio/ComponentsPreview.tsx +179 -0
  240. package/src/panels/studio/EveryUse.tsx +244 -0
  241. package/src/panels/studio/ThisUsePanel.tsx +679 -0
  242. package/src/panels/studio/componentParts.tsx +341 -0
  243. package/src/panels/studio/components.ts +6 -0
  244. package/src/panels/studio/newCodeComponent.ts +66 -0
  245. package/src/panels/timeline/AudioFxButton.tsx +290 -0
  246. package/src/panels/timeline/AudioLane.tsx +674 -85
  247. package/src/panels/timeline/BlockRow.tsx +154 -85
  248. package/src/panels/timeline/ChannelRow.tsx +378 -129
  249. package/src/panels/timeline/CurvePane.tsx +508 -0
  250. package/src/panels/timeline/Filmstrip.tsx +361 -0
  251. package/src/panels/timeline/GenerateDrawer.tsx +407 -0
  252. package/src/panels/timeline/KeyStretch.tsx +115 -0
  253. package/src/panels/timeline/LifespanRow.tsx +730 -47
  254. package/src/panels/timeline/MarkerLane.tsx +46 -8
  255. package/src/panels/timeline/PlayButton.tsx +91 -0
  256. package/src/panels/timeline/PropertyPicker.tsx +254 -0
  257. package/src/panels/timeline/ShotRow.tsx +400 -0
  258. package/src/panels/timeline/SoundMode.tsx +1406 -0
  259. package/src/panels/timeline/TimelineDock.tsx +2338 -272
  260. package/src/panels/timeline/TransportVolume.tsx +163 -0
  261. package/src/panels/timeline/beatRuler.ts +74 -0
  262. package/src/panels/timeline/curveModel.ts +500 -0
  263. package/src/panels/timeline/duckUnderVoice.ts +68 -0
  264. package/src/panels/timeline/keyframeMenu.ts +138 -0
  265. package/src/panels/timeline/labelWidth.ts +49 -0
  266. package/src/panels/timeline/marquee.ts +155 -0
  267. package/src/panels/timeline/model.ts +1860 -199
  268. package/src/panels/timeline/snap.ts +125 -43
  269. package/src/panels/timeline/staggerPreview.tsx +92 -0
  270. package/src/panels/timeline/takeMeasure.ts +72 -0
  271. package/src/panels/timeline/useLive.ts +48 -0
  272. package/src/panels/timeline/viewport.ts +234 -0
  273. package/src/panels/toolbar/BackChip.tsx +116 -0
  274. package/src/panels/toolbar/CameraViewBar.tsx +118 -49
  275. package/src/panels/toolbar/CanvasBar.tsx +117 -0
  276. package/src/panels/toolbar/InsertButton.tsx +38 -0
  277. package/src/panels/toolbar/IsolationTrail.tsx +156 -0
  278. package/src/panels/toolbar/PanelToggles.tsx +86 -0
  279. package/src/panels/toolbar/ShadingBar.tsx +119 -26
  280. package/src/panels/toolbar/ToolDockBar.tsx +329 -80
  281. package/src/panels/toolbar/ToolFooter.tsx +102 -0
  282. package/src/panels/toolbar/ViewModeBar.tsx +247 -38
  283. package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
  284. package/src/panels/toolbar/useAddRows.tsx +326 -0
  285. package/src/panels/toolbar/useDismiss.ts +122 -0
  286. package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
  287. package/src/panels/versions/versionModel.ts +103 -0
  288. package/src/panels/virtualWindow.ts +271 -0
  289. package/src/shotTokens.css +151 -60
  290. package/src/stage/FrustumGizmo.tsx +504 -84
  291. package/src/stage/GridPlane.tsx +225 -16
  292. package/src/stage/NodePreviews.tsx +628 -0
  293. package/src/stage/PerfHud.tsx +437 -47
  294. package/src/stage/SplitRenderer.tsx +816 -169
  295. package/src/stage/StageApp.tsx +4274 -486
  296. package/src/stage/StageRootBoundary.tsx +114 -0
  297. package/src/stage/StageTools.tsx +651 -151
  298. package/src/stage/TransformGizmo.tsx +696 -0
  299. package/src/stage/adaptivePresentation.ts +145 -0
  300. package/src/stage/bridgeClient.ts +2522 -41
  301. package/src/stage/exportWorker.ts +9 -0
  302. package/src/stage/feed.ts +305 -14
  303. package/src/stage/fontPreview.ts +176 -0
  304. package/src/stage/frustumGeometry.ts +319 -1
  305. package/src/stage/furnitureTokens.ts +33 -0
  306. package/src/stage/gizmoMath.ts +508 -16
  307. package/src/stage/gpuTimer.ts +276 -0
  308. package/src/stage/instanceModules.ts +194 -33
  309. package/src/stage/interactions/move.ts +24 -5
  310. package/src/stage/isolation.ts +291 -33
  311. package/src/stage/jsSelfProfiler.ts +325 -0
  312. package/src/stage/main.tsx +116 -16
  313. package/src/stage/modalState.ts +222 -23
  314. package/src/stage/moduleEpoch.ts +60 -0
  315. package/src/stage/motionPathStage.ts +279 -0
  316. package/src/stage/motionPreview.ts +74 -0
  317. package/src/stage/overlays/Overlays.tsx +1112 -159
  318. package/src/stage/overlays/Rulers.tsx +495 -0
  319. package/src/stage/panes.ts +159 -10
  320. package/src/stage/project.ts +68 -3
  321. package/src/stage/renderScaleController.ts +730 -0
  322. package/src/stage/rulers.ts +129 -0
  323. package/src/stage/sceneExtent.ts +75 -0
  324. package/src/stage/shading.ts +20 -0
  325. package/src/stage/splineStage.ts +632 -0
  326. package/src/stage/stagePerformanceTelemetry.ts +717 -0
  327. package/src/stage/stageTheme.ts +36 -0
  328. package/src/stage/stageWarmup.ts +11 -0
  329. package/src/stage/textAutoSize.ts +124 -0
  330. package/src/stage/tier1Export.ts +641 -0
  331. package/src/stage/tokenColor.ts +14 -9
  332. package/src/stage/view.ts +56 -9
  333. package/src/stage/workingView.ts +277 -33
  334. package/src/state/accountPrefs.ts +261 -0
  335. package/src/state/agentActions.ts +173 -15
  336. package/src/state/agentActivity.ts +198 -0
  337. package/src/state/agentQuestion.ts +239 -0
  338. package/src/state/bootOpen.ts +42 -0
  339. package/src/state/cameraVerbs.ts +139 -0
  340. package/src/state/clientId.ts +6 -1
  341. package/src/state/comments.ts +1099 -0
  342. package/src/state/context.tsx +19 -1
  343. package/src/state/editorPrefs.ts +220 -0
  344. package/src/state/errorReports.ts +121 -0
  345. package/src/state/fieldWrite.ts +161 -0
  346. package/src/state/fontRecents.ts +70 -0
  347. package/src/state/fonts.ts +656 -0
  348. package/src/state/graphClipboard.ts +414 -0
  349. package/src/state/imageThumbs.ts +216 -0
  350. package/src/state/insert.ts +444 -67
  351. package/src/state/isolationTrail.ts +92 -0
  352. package/src/state/keyMoves.ts +134 -0
  353. package/src/state/keyframeClipboard.ts +177 -0
  354. package/src/state/keymap.ts +1153 -117
  355. package/src/state/layoutPrefs.ts +272 -0
  356. package/src/state/layouts.ts +205 -0
  357. package/src/state/measureMedia.ts +167 -0
  358. package/src/state/menu.ts +2341 -28
  359. package/src/state/motionStamp.ts +7 -0
  360. package/src/state/panelSize.ts +375 -5
  361. package/src/state/pendingChanges.ts +119 -0
  362. package/src/state/playHold.ts +78 -0
  363. package/src/state/playbackPreparation.ts +562 -0
  364. package/src/state/posterCapture.ts +242 -0
  365. package/src/state/presence.ts +254 -0
  366. package/src/state/projectComponents.ts +160 -0
  367. package/src/state/renderEvents.ts +298 -18
  368. package/src/state/runtimeErrors.ts +46 -0
  369. package/src/state/selection.ts +12 -0
  370. package/src/state/shortcuts.ts +1606 -0
  371. package/src/state/sketchGuide.ts +51 -0
  372. package/src/state/splineRefs.ts +87 -0
  373. package/src/state/stageHealth.ts +259 -0
  374. package/src/state/stageWatchdog.ts +47 -0
  375. package/src/state/stillStage.ts +330 -0
  376. package/src/state/stillThumbs.ts +474 -0
  377. package/src/state/store.ts +23591 -2982
  378. package/src/state/storyboardModel.ts +528 -0
  379. package/src/state/sync.ts +1257 -0
  380. package/src/state/syncCache.ts +159 -0
  381. package/src/state/triangles.ts +13 -0
  382. package/src/state/types.ts +1698 -55
  383. package/src/state/unsavedGuard.ts +49 -0
  384. package/src/state/viewMode.ts +200 -0
  385. package/src/state/webmcp.ts +1128 -0
  386. package/src/state/workspace.ts +1147 -0
  387. package/src/styles/assets-tab.css +304 -0
  388. package/src/styles/canvas.css +545 -0
  389. package/src/styles/comments.css +1435 -0
  390. package/src/styles/controls.css +162 -0
  391. package/src/styles/dialogs.css +390 -0
  392. package/src/styles/insert-drawer.css +586 -0
  393. package/src/styles/inspector-board.css +207 -0
  394. package/src/styles/inspector-object.css +486 -0
  395. package/src/styles/inspector-shot.css +252 -0
  396. package/src/styles/inspector.css +360 -0
  397. package/src/styles/left-column.css +1628 -0
  398. package/src/styles/library-preview.css +575 -0
  399. package/src/styles/library.css +512 -0
  400. package/src/styles/modes/code.css +569 -0
  401. package/src/styles/modes/component.css +1561 -0
  402. package/src/styles/modes/material.css +1201 -0
  403. package/src/styles/modes/storyboard.css +518 -0
  404. package/src/styles/share-component.css +49 -0
  405. package/src/styles/shell.css +653 -0
  406. package/src/styles/shot-brief.css +360 -0
  407. package/src/styles/states.css +76 -0
  408. package/src/styles/sync.css +380 -0
  409. package/src/styles/timeline-graph.css +384 -0
  410. package/src/styles/timeline-meters.css +89 -0
  411. package/src/styles/timeline-rows.css +1959 -0
  412. package/src/styles/timeline-sound.css +1003 -0
  413. package/src/styles/timeline.css +754 -0
  414. package/src/styles/tool-footer.css +525 -0
  415. package/src/styles/topbar.css +886 -0
  416. package/src/styles/versions.css +262 -0
  417. package/src/styles.css +72 -2921
  418. package/src/tools.ts +108 -23
  419. package/dist/assets/index-B7notge7.css +0 -1
  420. package/dist/assets/index-BBE6Ta9L.js +0 -52
  421. package/src/panels/files/icons.tsx +0 -118
  422. package/src/panels/header/DocSwitcher.tsx +0 -85
  423. package/src/panels/header/TransportBar.tsx +0 -86
  424. package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
  425. package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
  426. package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
  427. package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
  428. package/src/panels/inspector/animate/useBlocks.ts +0 -43
  429. package/src/panels/material/MaterialPicker.tsx +0 -172
  430. package/src/panels/timeline/SceneStrip.tsx +0 -145
  431. package/src/panels/toolbar/AddMenu.tsx +0 -230
  432. package/src/stage/MoveGizmo.tsx +0 -411
@@ -1,66 +1,709 @@
1
- // The File tab's MCP section: how to attach a coding agent to this workspace.
2
- // Lives where History used to — connecting an agent is a thing an author does
3
- // every session, and buried at the bottom of the collapsed Manual nobody
4
- // found it.
1
+ // Workspace MCP readiness and setup: the top bar's Agent chip opens it in a
2
+ // `Popover` (AgentChip.tsx), and Help repeats it behind a disclosure.
5
3
  //
6
- // On a CLOUD workspace the Worker serves `GET /auth/mcp-token` (cookie-authed)
7
- // with a ready-to-paste `claude mcp add` command scoped to this user's own
8
- // /mcp endpoint — render that with a Copy button. A local server has no such
9
- // route, so any non-JSON answer, non-200, or missing field means "local": show
10
- // the `connect` instructions. The fallback needs no cloud-detection meta — the
11
- // route's absence IS the signal.
12
- import { useEffect, useState } from "react";
13
-
14
- export function McpSection() {
15
- const [command, setCommand] = useState<string | null>(null);
16
- const [copied, setCopied] = useState(false);
4
+ // TWO PRESENTATIONS OF ONE STATE. The chip owns the health probe
5
+ // (`useMcpHealth`) and hands it to `McpPopover`, so the chip's word and the
6
+ // details under it never disagree. Help's `McpSection` probes for itself,
7
+ // because Help is a modal dialog and a popover inside it would be a floating
8
+ // layer over a floating layer. The left column used to carry a 64px "band"
9
+ // with the same details; the chip replaced it, so the column's rows are the
10
+ // author's again.
11
+ //
12
+ // The live server owns the status contract:
13
+ // `GET /api/health` says whether `/mcp` is mounted and whether direct document
14
+ // writes are enabled. It does not report an active client, so this UI never
15
+ // claims one is connected.
16
+ //
17
+ // MCP is workspace-scoped. The current Project title and storage name are a
18
+ // target hint for the agent, not an access boundary. Hosted, an AI app connects
19
+ // to the MCP URL on MAIN and signs in with the member's Dailies account (worker
20
+ // oauth.ts). The connect rows show that URL, read from the discovery document
21
+ // this host serves, and link to the dashboard's Connect your AI page for each
22
+ // app's steps, the connected apps and the tokens for clients without OAuth.
23
+ // The editor origin cannot reach `/api/mcp`, so no token is made or shown here.
24
+ //
25
+ // Hosted, the write half is the owner's switch here: it writes `mcp.write` into
26
+ // the workspace's motion.config.json through the ordinary file route, against
27
+ // the hash it read, keeping every other key. A hosted workspace has no terminal
28
+ // to run `connect --write` in, and this is the same deliberate second act.
29
+ import { useEffect, useRef, useState } from "react";
30
+ import type { RefObject } from "react";
31
+ import { IconCheck, IconChevron, IconLink, IconLinkOff, Popover, Toggle } from "@dailies-studio/ui";
32
+ import { isCloudWorkspace } from "../../state/posterCapture.js";
33
+ import { isLoopbackHostname } from "../../stage/jsSelfProfiler.js";
34
+
35
+ const CONNECT_COMMAND = "npx dailies-studio connect";
36
+ const WRITE_COMMAND = "npx dailies-studio connect --write";
37
+ const STATUS_COMMAND = "dailies status";
38
+ /** The CLI's `CONFIG_FILE`, which the hosted MCP surface reads its write half
39
+ * from (worker mcpConfig.ts). */
40
+ export const MCP_CONFIG_FILE = "motion.config.json";
41
+ const WRITES_LABEL = "Let agents edit this workspace";
42
+ /** A client lists its tools when it connects, and this server can tell no
43
+ * connected client that the list changed (it keeps no stream). */
44
+ export const RECONNECT_NOTE =
45
+ "A client connected before a change keeps its old tools until it reconnects: in Claude Code, run /mcp and reconnect dailies.";
46
+ /** The `.mcp.json` server key the CLI writes (connectVerbs.ts MCP_SERVER_KEY)
47
+ * and the name the dashboard's command registers. */
48
+ const MCP_SERVER_KEY = "dailies";
49
+ /** The dashboard's Connect your AI page (dashboard Sidebar.tsx `View`). Relative
50
+ * on purpose: the editor host knows no MAIN origin, and it redirects every
51
+ * `/projects` path, query kept, to MAIN (worker index.ts `hostRoute`), the
52
+ * same way the account menu reaches the dashboard. */
53
+ export const AGENT_ACCESS_HREF = "/projects/?view=settings-agents";
54
+ /** `WORKER_HEADERS.mcpToken` / `CLOUD_HEADERS.mcpToken`. Chrome imports
55
+ * neither package, so the name is repeated here. */
56
+ const MCP_TOKEN_HEADER = "x-studio-mcp-token";
57
+ /** The env var a non-loopback studio requires (server.ts `mcpMountable`). The
58
+ * rows name it and never carry a value. */
59
+ const MCP_TOKEN_ENV = "STUDIO_MCP_TOKEN";
60
+
61
+ export type McpHealth =
62
+ | { kind: "checking" }
63
+ | { kind: "unavailable" }
64
+ /** `allowed`: whether this caller may use MCP (`mcp.connect`); `manage`:
65
+ * whether it may flip the write half (`mcp.manage`, Owner or Admin). Both
66
+ * from worker `storeHealth`; a local server has no roles, so both true. */
67
+ | { kind: "ready"; mounted: boolean; write: boolean; allowed: boolean; manage: boolean };
68
+
69
+ /** A hosted role without `mcp.connect` (Billing, or a paused seat): `/mcp` is
70
+ * there, but it refuses this caller, so nothing that leads to it is offered. */
71
+ export function adminsOnly(health: McpHealth): boolean {
72
+ return health.kind === "ready" && health.mounted && !health.allowed;
73
+ }
74
+
75
+ /** The same sentence as the dashboard's refusal (ConnectAI.tsx). */
76
+ export const ADMINS_ONLY_NOTE =
77
+ "Your role in this workspace cannot connect an AI. An Owner or an Admin can change your role.";
78
+
79
+ /** The details layer's floor, so a chip in a short window still opens
80
+ * something readable, and the gap it keeps from the viewport's bottom edge
81
+ * plus the Popover's own 4px offset. */
82
+ const POPOVER_MIN_H = 160;
83
+ const POPOVER_EDGE = 12;
84
+
85
+ /**
86
+ * The one health probe: read on mount and whenever the Project changes, and
87
+ * again whenever the author comes back to the tab, since the switch, a file
88
+ * edit or an agent may have changed motion.config.json meanwhile. A panel still
89
+ * saying "Direct document writes enabled" over a file the workspace can no
90
+ * longer read would be a claim nothing backs.
91
+ */
92
+ export function useMcpHealth(projectName: string | null): { health: McpHealth; retry: () => void } {
93
+ const [attempt, setAttempt] = useState(0);
94
+ const [health, setHealth] = useState<McpHealth>({ kind: "checking" });
17
95
 
18
96
  useEffect(() => {
19
97
  let alive = true;
20
- void (async () => {
98
+ // A ready answer stays on screen while it is read again (after the write
99
+ // switch saved, say), so the switch and what it reported do not blink out.
100
+ setHealth((current) => (current.kind === "ready" ? current : { kind: "checking" }));
101
+ void readHealth().then((next) => {
102
+ if (alive) setHealth(next);
103
+ });
104
+ return () => {
105
+ alive = false;
106
+ };
107
+ }, [attempt, projectName]);
108
+
109
+ useEffect(() => {
110
+ const again = (): void => setAttempt((value) => value + 1);
111
+ window.addEventListener("focus", again);
112
+ return () => window.removeEventListener("focus", again);
113
+ }, []);
114
+
115
+ return { health, retry: () => setAttempt((value) => value + 1) };
116
+ }
117
+
118
+ /** The chip's word: health only. "MCP ready" means writes are on, "Read only"
119
+ * that the server answers but edits nothing, "MCP off" that `/mcp` is not
120
+ * mounted, "No access" that this caller's role may not use it. Never
121
+ * "connected": health cannot see a client. */
122
+ export function mcpWord(health: McpHealth): string {
123
+ if (health.kind === "checking") return "Checking MCP";
124
+ if (health.kind === "unavailable") return "Status unknown";
125
+ if (!health.mounted) return "MCP off";
126
+ if (adminsOnly(health)) return "No access";
127
+ return health.write ? "MCP ready" : "Read only";
128
+ }
129
+
130
+ interface ProjectProps {
131
+ projectName?: string | null;
132
+ projectTitle?: string | null;
133
+ }
134
+
135
+ /** Help's presentation: the details behind a disclosure, with a probe of its
136
+ * own. `Dialog` returns null while closed, so it reads health only once the
137
+ * author opens Help. */
138
+ export function McpSection({ projectName = null, projectTitle = null }: ProjectProps) {
139
+ const { health, retry } = useMcpHealth(projectName);
140
+ return (
141
+ <details className="mcp-section" data-testid="mcp-section">
142
+ <summary>
143
+ {/* The house chevron, not a `"›"` glyph in a `::before`. Two
144
+ disclosure shapes in one surface is one more than a reader can be
145
+ asked to learn, and the outline already draws this icon. */}
146
+ <IconChevron className="chevron" />
147
+ <span className="mcp-heading">
148
+ <IconLink />
149
+ <span>MCP connection</span>
150
+ </span>
151
+ <McpStatus health={health} />
152
+ </summary>
153
+ <McpBody health={health} onRetry={retry} projectName={projectName} projectTitle={projectTitle} />
154
+ </details>
155
+ );
156
+ }
157
+
158
+ /**
159
+ * The chip's presentation: the same body in a `Popover` under `anchor`. The
160
+ * caller owns `open` and the probe; this owns focus.
161
+ *
162
+ * FOCUS GOES INTO THE DETAILS, and comes back to the anchor. The popover is
163
+ * portaled to the end of <body>, so without this a Tab from the chip walks the
164
+ * rest of the shell and reaches the switch, Retry and Copy last of all. A
165
+ * timeout rather than the effect itself: `Popover` holds the layer at
166
+ * `visibility: hidden` until its layout effect has measured it, and `focus()`
167
+ * on a hidden element is a no-op. That re-render is flushed in the same task,
168
+ * so one macrotask later the layer is shown.
169
+ */
170
+ export function McpPopover({
171
+ anchor,
172
+ open,
173
+ onClose,
174
+ health,
175
+ onRetry,
176
+ projectName = null,
177
+ projectTitle = null,
178
+ handoff,
179
+ }: ProjectProps & {
180
+ anchor: RefObject<HTMLElement | null>;
181
+ open: boolean;
182
+ onClose: () => void;
183
+ health: McpHealth;
184
+ onRetry: () => void;
185
+ /** The prompt to copy for an agent, built at the click (agentHandoff.ts). */
186
+ handoff?: () => string | null;
187
+ }) {
188
+ const details = useRef<HTMLDivElement>(null);
189
+ const [cap, setCap] = useState(POPOVER_MIN_H);
190
+
191
+ useEffect(() => {
192
+ if (!open) return;
193
+ // The layer opens BELOW the chip, and `Popover` only flips up when the
194
+ // whole layer fits there. So it is capped to the room under the anchor,
195
+ // measured at the open; a long body scrolls inside it.
196
+ const bottom = anchor.current?.getBoundingClientRect().bottom ?? 0;
197
+ setCap(Math.max(POPOVER_MIN_H, window.innerHeight - bottom - POPOVER_EDGE));
198
+ const id = window.setTimeout(() => details.current?.focus(), 0);
199
+ return () => window.clearTimeout(id);
200
+ }, [open, anchor]);
201
+
202
+ /** Every way out of the details. Focus that was inside them goes back to
203
+ * the anchor, because the node holding it is about to unmount and would
204
+ * leave it on <body>. Focus elsewhere (an outside press) stays where the
205
+ * author put it. */
206
+ const close = (): void => {
207
+ const inside = details.current?.contains(document.activeElement) === true;
208
+ onClose();
209
+ if (inside) anchor.current?.focus();
210
+ };
211
+
212
+ return (
213
+ <Popover open={open} anchor={anchor} onClose={close} align="right" className="mcp-popover">
214
+ <div
215
+ ref={details}
216
+ tabIndex={-1}
217
+ className="mcp-popover-body"
218
+ role="dialog"
219
+ aria-label="MCP connection"
220
+ data-testid="mcp-details"
221
+ style={{ maxHeight: `${cap}px` }}
222
+ >
223
+ <McpBody
224
+ health={health}
225
+ onRetry={onRetry}
226
+ projectName={projectName}
227
+ projectTitle={projectTitle}
228
+ handoff={handoff}
229
+ />
230
+ </div>
231
+ </Popover>
232
+ );
233
+ }
234
+
235
+ /** A copy that says "Copied" on the row it came from. The value stays
236
+ * selectable when clipboard permission is refused. */
237
+ export function useCopy(resetKey: unknown = null): { copied: string | null; copy: (target: string, value: string) => void } {
238
+ const [copied, setCopied] = useState<string | null>(null);
239
+ // A "Copied" names what was copied, so it clears when that changes.
240
+ useEffect(() => setCopied(null), [resetKey]);
241
+ const mounted = useRef(true);
242
+ useEffect(() => {
243
+ mounted.current = true;
244
+ return () => {
245
+ mounted.current = false;
246
+ };
247
+ }, []);
248
+ const copy = (target: string, value: string): void => {
249
+ const clipboard = navigator.clipboard;
250
+ if (clipboard === undefined) return;
251
+ void clipboard.writeText(value).then(
252
+ () => {
253
+ if (mounted.current) setCopied(target);
254
+ },
255
+ () => {},
256
+ );
257
+ };
258
+ return { copied, copy };
259
+ }
260
+
261
+ function McpBody({
262
+ health,
263
+ onRetry,
264
+ projectName = null,
265
+ projectTitle = null,
266
+ handoff,
267
+ }: ProjectProps & { health: McpHealth; onRetry: () => void; handoff?: (() => string | null) | undefined }) {
268
+ const hosted = isCloudWorkspace();
269
+ const { copied, copy } = useCopy(projectName);
270
+ const title = projectTitle?.trim() || projectName?.trim() || null;
271
+
272
+ return (
273
+ <div className="mcp-body">
274
+ {/* A Member has no agent to hand a storage name or a prompt to. */}
275
+ {title === null || adminsOnly(health) ? null : (
276
+ <div className="mcp-project" data-testid="mcp-project-target">
277
+ <span className="mcp-kicker">Project target</span>
278
+ <strong>{title}</strong>
279
+ {projectName === null ? null : (
280
+ <div className="mcp-project-name">
281
+ <span>Storage name</span>
282
+ <code>{projectName}</code>
283
+ <CopyButton copied={copied === "project"} onClick={() => copy("project", projectName)} />
284
+ </div>
285
+ )}
286
+ <p>An agent sees the whole workspace. Give it this storage name to work on this Project.</p>
287
+ {handoff === undefined ? null : (
288
+ <div className="mcp-handoff" data-testid="mcp-handoff">
289
+ <span>A prompt naming this Shot, frame and selection</span>
290
+ <CopyButton
291
+ copied={copied === "handoff"}
292
+ onClick={() => {
293
+ const prompt = handoff();
294
+ if (prompt !== null) copy("handoff", prompt);
295
+ }}
296
+ />
297
+ </div>
298
+ )}
299
+ </div>
300
+ )}
301
+
302
+ <Readiness health={health} hosted={hosted} onRetry={onRetry} />
303
+
304
+ {hosted && health.kind === "ready" && health.mounted && health.allowed && health.manage ? (
305
+ <HostedWrites write={health.write} onSaved={onRetry} />
306
+ ) : null}
307
+
308
+ <InTabTools />
309
+
310
+ <ConnectRows health={health} />
311
+ </div>
312
+ );
313
+ }
314
+
315
+ /**
316
+ * The OTHER surface: the tools this page registers for an agent running in the
317
+ * browser (WebMCP, `state/webmcp.ts`).
318
+ *
319
+ * Read once at render and not subscribed: `document.modelContext` either exists
320
+ * when the page loads or does not, and the registration happens before React
321
+ * commits. A secure context is required, so a dev server reached over the LAN by
322
+ * plain http reads as unsupported even in a browser that has the API.
323
+ */
324
+ function InTabTools() {
325
+ const supported = typeof document.modelContext !== "undefined";
326
+ return (
327
+ <p className="mcp-note" data-testid="mcp-webmcp">
328
+ In-tab tools (WebMCP):{" "}
329
+ {supported
330
+ ? "on. A browser agent can read and edit this Project in this tab."
331
+ : "not in this browser. Nothing else changes."}
332
+ </p>
333
+ );
334
+ }
335
+
336
+ /** The status pill at the end of Help's summary, in its one-word forms. */
337
+ function McpStatus({ health }: { health: McpHealth }) {
338
+ if (health.kind === "checking") return <span className="mcp-status">Checking</span>;
339
+ if (health.kind === "unavailable") {
340
+ return (
341
+ <span className="mcp-status is-unknown" role="status">
342
+ <IconLinkOff /> Unknown
343
+ </span>
344
+ );
345
+ }
346
+ if (!health.mounted || adminsOnly(health)) {
347
+ return (
348
+ <span className="mcp-status is-off" role="status">
349
+ <IconLinkOff /> {health.mounted ? "No access" : "Off"}
350
+ </span>
351
+ );
352
+ }
353
+ return (
354
+ <span className={`mcp-status${health.write ? " is-write" : " is-read"}`} role="status">
355
+ {health.write ? <IconCheck /> : <IconLink />}
356
+ {health.write ? "Write tools enabled" : "Read only"}
357
+ </span>
358
+ );
359
+ }
360
+
361
+ function Readiness({ health, hosted, onRetry }: { health: McpHealth; hosted: boolean; onRetry: () => void }) {
362
+ if (health.kind === "checking") {
363
+ return <p className="mcp-note">{hosted ? "Checking this workspace." : "Checking the live Studio server."}</p>;
364
+ }
365
+ if (health.kind === "unavailable") {
366
+ return (
367
+ <div className="mcp-readiness is-unknown">
368
+ <div>
369
+ <strong>{hosted ? "Couldn't check this workspace" : "Couldn't check the live server"}</strong>
370
+ {hosted ? (
371
+ <span>The workspace did not answer. Retry, or reload the editor.</span>
372
+ ) : (
373
+ <span>Run <code>{STATUS_COMMAND}</code> in the workspace to inspect it from the terminal.</span>
374
+ )}
375
+ </div>
376
+ <button type="button" onClick={onRetry}>Retry</button>
377
+ </div>
378
+ );
379
+ }
380
+ if (!health.mounted) {
381
+ return (
382
+ <div className="mcp-readiness is-off">
383
+ <IconLinkOff />
384
+ <div>
385
+ <strong>{hosted ? "MCP is not available here" : "MCP is off on this running server"}</strong>
386
+ <span>
387
+ {hosted
388
+ ? "This deployment does not serve MCP, so no AI can connect to this workspace."
389
+ : "Configure it below, then restart the Studio server."}
390
+ </span>
391
+ </div>
392
+ </div>
393
+ );
394
+ }
395
+ if (adminsOnly(health)) {
396
+ return (
397
+ <div className="mcp-readiness is-off" data-testid="mcp-admins-only">
398
+ <IconLinkOff />
399
+ <div>
400
+ <strong>MCP is not open to your role</strong>
401
+ <span>{ADMINS_ONLY_NOTE}</span>
402
+ </div>
403
+ </div>
404
+ );
405
+ }
406
+ return (
407
+ <div className={`mcp-readiness${health.write ? " is-write" : " is-read"}`}>
408
+ {health.write ? <IconCheck /> : <IconLink />}
409
+ <div>
410
+ <strong>MCP server ready</strong>
411
+ <span>
412
+ {health.write
413
+ ? "Direct document writes enabled."
414
+ : hosted
415
+ ? health.manage
416
+ ? `Dailies documents: read-only. Turn on "${WRITES_LABEL}" below to let an agent edit.`
417
+ : "Dailies documents: read-only. An Owner or an Admin can turn on AI edits."
418
+ : "Dailies documents: read-only."}
419
+ </span>
420
+ </div>
421
+ </div>
422
+ );
423
+ }
424
+
425
+ /**
426
+ * The hosted write switch. It shows what the workspace answers now (health),
427
+ * and a flip writes `mcp.write` into motion.config.json (`setMcpWrites`). The
428
+ * health is read again either way, so a refused or conflicting save leaves the
429
+ * switch showing what is actually saved.
430
+ */
431
+ function HostedWrites({ write, onSaved }: { write: boolean; onSaved: () => void }) {
432
+ const [busy, setBusy] = useState(false);
433
+ const [problem, setProblem] = useState<string | null>(null);
434
+ const alive = useRef(true);
435
+ useEffect(() => {
436
+ alive.current = true;
437
+ return () => {
438
+ alive.current = false;
439
+ };
440
+ }, []);
441
+ const flip = (next: boolean): void => {
442
+ setBusy(true);
443
+ setProblem(null);
444
+ void setMcpWrites(next).then((refused) => {
445
+ if (!alive.current) return;
446
+ setBusy(false);
447
+ setProblem(refused);
448
+ onSaved();
449
+ });
450
+ };
451
+ return (
452
+ <div className="mcp-writes" data-testid="mcp-hosted-writes">
453
+ <div>
454
+ <strong>{WRITES_LABEL}</strong>
455
+ <span>
456
+ {write
457
+ ? "An agent can change Projects, storyboards, code and assets here."
458
+ : "An agent can read this workspace but change nothing."}
459
+ </span>
460
+ </div>
461
+ <Toggle checked={write} disabled={busy} ariaLabel={WRITES_LABEL} onChange={flip} />
462
+ <p className="mcp-note" data-testid="mcp-hosted-reconnect">{RECONNECT_NOTE}</p>
463
+ {problem === null ? null : (
464
+ <p className="mcp-writes-problem" role="alert">
465
+ {problem}
466
+ </p>
467
+ )}
468
+ </div>
469
+ );
470
+ }
471
+
472
+ /**
473
+ * Write `mcp.write` into the workspace's motion.config.json, keeping every
474
+ * other key, through `/api/files`: PUT against the hash just read, or POST
475
+ * (create) when there is no file, which then holds only this key. It never
476
+ * rewrites a file it cannot parse and never merges: a file that moved in
477
+ * between answers 409, which is reported. Null when saved, else why not.
478
+ */
479
+ export async function setMcpWrites(write: boolean): Promise<string | null> {
480
+ const url = `/api/files/${MCP_CONFIG_FILE}`;
481
+ let config: Record<string, unknown> = {};
482
+ let baseHash: string | undefined;
483
+ try {
484
+ const read = await fetch(url, { cache: "no-store", headers: { accept: "application/json" } });
485
+ if (read.ok) {
486
+ const file = (await read.json()) as { content?: unknown; hash?: unknown };
487
+ if (typeof file.content !== "string" || typeof file.hash !== "string") return `Couldn't read ${MCP_CONFIG_FILE}.`;
488
+ let parsed: unknown = null;
21
489
  try {
22
- const res = await fetch("/auth/mcp-token", { headers: { accept: "application/json" } });
23
- if (!res.ok) return;
24
- const body = (await res.json()) as { command?: unknown };
25
- if (alive && typeof body.command === "string") setCommand(body.command);
490
+ parsed = JSON.parse(file.content);
26
491
  } catch {
27
- // Local server, offline, or an HTML answer — the static text below
28
- // is already the right instruction set.
492
+ /* refused below */
493
+ }
494
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
495
+ return `${MCP_CONFIG_FILE} is not a JSON object, so it was left as it is. Fix it in the file tree, then try again.`;
29
496
  }
30
- })();
497
+ config = parsed as Record<string, unknown>;
498
+ baseHash = file.hash;
499
+ } else if (read.status !== 404) {
500
+ return `Couldn't read ${MCP_CONFIG_FILE} (${read.status}).`;
501
+ }
502
+ const mcp = config["mcp"];
503
+ const next = {
504
+ ...config,
505
+ mcp: { ...(typeof mcp === "object" && mcp !== null && !Array.isArray(mcp) ? mcp : {}), write },
506
+ };
507
+ const content = `${JSON.stringify(next, null, 2)}\n`;
508
+ const saved = await fetch(url, {
509
+ method: baseHash === undefined ? "POST" : "PUT",
510
+ headers: { "content-type": "application/json", accept: "application/json" },
511
+ body: JSON.stringify(baseHash === undefined ? { content } : { content, baseHash }),
512
+ });
513
+ if (saved.ok) return null;
514
+ if (saved.status === 409) {
515
+ return `${MCP_CONFIG_FILE} changed while this was being saved. The switch shows the saved setting now; flip it again if that is not what you want.`;
516
+ }
517
+ const body = (await saved.json().catch(() => null)) as { error?: unknown } | null;
518
+ return `Couldn't save ${MCP_CONFIG_FILE}${typeof body?.error === "string" ? `: ${body.error}` : ` (${saved.status})`}.`;
519
+ } catch {
520
+ return `Couldn't reach the workspace to save ${MCP_CONFIG_FILE}.`;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * What to paste into a client, for the local origin this page was served
526
+ * from: the studio server or the chrome's dev server (which proxies `/mcp`,
527
+ * vite.config.ts). Hosted has no rows: an app adds the MCP URL and signs in
528
+ * (`HostedConnect`).
529
+ *
530
+ * - `claude`: Claude Code's `claude mcp add`. Off loopback the studio mounts
531
+ * `/mcp` only behind `STUDIO_MCP_TOKEN`, so the header names the variable,
532
+ * never its value, in DOUBLE quotes: the author's shell fills it in at the
533
+ * add. Claude Code stores the header as given and never expands a bare
534
+ * `$VAR`, so single quotes would register a header that always fails.
535
+ * - `json`: the `.mcp.json` entry `connect --http` writes (connectVerbs.ts),
536
+ * with the header as Claude Code's `${VAR}` expansion off loopback.
537
+ */
538
+ export function connectCommands(where: { origin: string; hostname: string }): { claude: string; json: string } {
539
+ const url = `${where.origin}/mcp`;
540
+ const add = `claude mcp add --transport http ${MCP_SERVER_KEY} ${url}`;
541
+ const loopback = isLoopbackHostname(where.hostname);
542
+ const entry = {
543
+ type: "http",
544
+ url,
545
+ ...(loopback ? {} : { headers: { [MCP_TOKEN_HEADER]: `\${${MCP_TOKEN_ENV}}` } }),
546
+ };
547
+ return {
548
+ claude: loopback ? add : `${add} --header "${MCP_TOKEN_HEADER}: $${MCP_TOKEN_ENV}"`,
549
+ json: JSON.stringify({ mcpServers: { [MCP_SERVER_KEY]: entry } }),
550
+ };
551
+ }
552
+
553
+ /**
554
+ * The connect rows. Local: `connect` first while `/mcp` is not known to be
555
+ * mounted (it writes the config and the entry), then the Claude Code command
556
+ * and the `.mcp.json` entry for this origin. Hosted: the MCP URL and a link to
557
+ * the dashboard's Connect your AI page, in a new tab so the editor stays open,
558
+ * unless `/mcp` is not mounted there or refuses this role (Readiness says
559
+ * why), so no step is offered that cannot work.
560
+ */
561
+ export function ConnectRows({ health }: { health: McpHealth }) {
562
+ const hosted = isCloudWorkspace();
563
+ const { copied, copy } = useCopy();
564
+
565
+ if (hosted) {
566
+ if ((health.kind === "ready" && !health.mounted) || adminsOnly(health)) return null;
567
+ return <HostedConnect />;
568
+ }
569
+
570
+ const rows = connectCommands(window.location);
571
+ const mounted = health.kind === "ready" && health.mounted;
572
+ return (
573
+ <div className="mcp-setup" data-testid="mcp-local-setup">
574
+ {mounted ? null : (
575
+ <>
576
+ <p className="mcp-note">
577
+ Run the helper from this workspace root first. It updates <code>motion.config.json</code> and{" "}
578
+ <code>.mcp.json</code>.
579
+ </p>
580
+ <CommandRow
581
+ label="Read-only setup"
582
+ value={CONNECT_COMMAND}
583
+ copied={copied === "connect"}
584
+ onCopy={() => copy("connect", CONNECT_COMMAND)}
585
+ />
586
+ </>
587
+ )}
588
+ <CommandRow
589
+ label="Claude Code"
590
+ value={rows.claude}
591
+ copied={copied === "claude"}
592
+ onCopy={() => copy("claude", rows.claude)}
593
+ />
594
+ <CommandRow
595
+ label=".mcp.json"
596
+ value={rows.json}
597
+ copied={copied === "json"}
598
+ onCopy={() => copy("json", rows.json)}
599
+ />
600
+ <p className="mcp-footnote">
601
+ Read-only is the default. To allow direct Project document edits, run <code>{WRITE_COMMAND}</code>, then restart
602
+ the Studio server. Verify the live state with <code>{STATUS_COMMAND}</code>.
603
+ </p>
604
+ </div>
605
+ );
606
+ }
607
+
608
+ /** The discovery document every host serves (worker oauth.ts). Its
609
+ * `resource` is the MCP URL on MAIN, which the editor host cannot name. */
610
+ const RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/mcp";
611
+
612
+ /** The MCP URL, or null until read or when the read fails. */
613
+ function useMcpUrl(): string | null {
614
+ const [url, setUrl] = useState<string | null>(null);
615
+ useEffect(() => {
616
+ let alive = true;
617
+ void readJson(RESOURCE_METADATA_PATH).then((body) => {
618
+ const resource = (body as { resource?: unknown } | null)?.resource;
619
+ if (alive && typeof resource === "string" && /^https?:\/\/[^/]+\/mcp$/.test(resource)) setUrl(resource);
620
+ });
31
621
  return () => {
32
622
  alive = false;
33
623
  };
34
624
  }, []);
625
+ return url;
626
+ }
35
627
 
628
+ /** Hosted: the URL to add to an AI app, and the page with every app's steps.
629
+ * Without the URL (the read failed), the link alone still gets there. */
630
+ function HostedConnect() {
631
+ const url = useMcpUrl();
632
+ const { copied, copy } = useCopy();
36
633
  return (
37
- <div className="mcp-section" data-testid="mcp-section">
38
- <div className="file-section-label">MCP</div>
39
- {command === null ? (
40
- <p className="help-note">
41
- Run <code>npx dailies-studio connect</code> in this project, then start Claude Code in the
42
- same folder — it picks up <code>.mcp.json</code> and can edit this scene.
43
- </p>
44
- ) : (
634
+ <div className="mcp-setup" data-testid="mcp-hosted-setup">
635
+ {url === null ? null : (
45
636
  <>
46
- <p className="help-note">
47
- Paste into your terminal — Claude Code connects to this workspace over MCP:
48
- </p>
49
- <div className="help-command">
50
- <code>{command}</code>
51
- <button
52
- type="button"
53
- onClick={() => {
54
- void navigator.clipboard?.writeText(command);
55
- setCopied(true);
56
- window.setTimeout(() => setCopied(false), 1500);
57
- }}
58
- >
59
- {copied ? "Copied" : "Copy"}
60
- </button>
61
- </div>
637
+ <CommandRow label="Server URL" value={url} copied={copied === "url"} onCopy={() => copy("url", url)} />
638
+ <p className="mcp-note">Add this URL to your AI. It signs in with your Dailies account.</p>
62
639
  </>
63
640
  )}
641
+ <a className="mcp-connect-link" href={AGENT_ACCESS_HREF} target="_blank" rel="noopener">
642
+ Connect your AI: steps for each app
643
+ </a>
64
644
  </div>
65
645
  );
66
646
  }
647
+
648
+ export function CommandRow({
649
+ label,
650
+ value,
651
+ copied,
652
+ onCopy,
653
+ }: {
654
+ label: string;
655
+ value: string;
656
+ copied: boolean;
657
+ onCopy: () => void;
658
+ }) {
659
+ return (
660
+ <div className="mcp-command">
661
+ <span>{label}</span>
662
+ <div>
663
+ <code>{value}</code>
664
+ <CopyButton copied={copied} onClick={onCopy} />
665
+ </div>
666
+ </div>
667
+ );
668
+ }
669
+
670
+ export function CopyButton({ copied, onClick }: { copied: boolean; onClick: () => void }) {
671
+ return (
672
+ <button type="button" onClick={onClick} aria-live="polite">
673
+ {copied ? "Copied" : "Copy"}
674
+ </button>
675
+ );
676
+ }
677
+
678
+ export async function readHealth(): Promise<McpHealth> {
679
+ const body = await readJson("/api/health");
680
+ if (typeof body !== "object" || body === null) return { kind: "unavailable" };
681
+ const record = body as { ok?: unknown; name?: unknown; mcp?: unknown };
682
+ // Chrome cannot import the CLI, so this repeats the two-name test in
683
+ // `probeStudioHealth` (packages/cli/src/devServerLock.ts). That is the source
684
+ // this must match.
685
+ if (record.ok !== true || (record.name !== "dailies" && record.name !== "motion-studio")) {
686
+ return { kind: "unavailable" };
687
+ }
688
+ if (typeof record.mcp !== "object" || record.mcp === null) return { kind: "unavailable" };
689
+ const mcp = record.mcp as { mounted?: unknown; write?: unknown; allowed?: unknown; manage?: unknown };
690
+ if (typeof mcp.mounted !== "boolean" || typeof mcp.write !== "boolean") return { kind: "unavailable" };
691
+ // A local server has no roles and sends neither.
692
+ return {
693
+ kind: "ready",
694
+ mounted: mcp.mounted,
695
+ write: mcp.write,
696
+ allowed: mcp.allowed !== false,
697
+ manage: mcp.manage !== false,
698
+ };
699
+ }
700
+
701
+ async function readJson(path: string): Promise<unknown | null> {
702
+ try {
703
+ const response = await fetch(path, { headers: { accept: "application/json" } });
704
+ if (!response.ok) return null;
705
+ return await response.json();
706
+ } catch {
707
+ return null;
708
+ }
709
+ }