auricui-skill 0.0.0-stage → 0.1.2

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 (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/install.mjs +66 -0
  4. package/package.json +29 -4
  5. package/skill/SKILL.md +64 -0
  6. package/skill/agents/openai.yaml +4 -0
  7. package/skill/assets/examples/ApprovalCard.tsx +8 -0
  8. package/skill/assets/examples/BasicApp.tsx +38 -0
  9. package/skill/assets/examples/BriefingCard.tsx +8 -0
  10. package/skill/assets/examples/ChatControls.tsx +49 -0
  11. package/skill/assets/examples/ChatPanel.tsx +73 -0
  12. package/skill/assets/examples/Checkbox.tsx +13 -0
  13. package/skill/assets/examples/Combobox.tsx +14 -0
  14. package/skill/assets/examples/Drawer.tsx +10 -0
  15. package/skill/assets/examples/MemoryCard.tsx +9 -0
  16. package/skill/assets/examples/MessageList.tsx +8 -0
  17. package/skill/assets/examples/Theme.tsx +14 -0
  18. package/skill/assets/examples/ToolCall.tsx +8 -0
  19. package/skill/assets/examples/Tree.tsx +15 -0
  20. package/skill/assets/examples/examples.css +11 -0
  21. package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
  22. package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
  23. package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
  24. package/skill/assets/upstream/LICENSE +21 -0
  25. package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
  26. package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
  27. package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
  28. package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
  29. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
  30. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
  31. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
  32. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
  33. package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
  34. package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
  35. package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
  36. package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
  37. package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
  38. package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
  39. package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
  40. package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
  41. package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
  42. package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
  43. package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
  44. package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
  45. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
  46. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
  47. package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
  48. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
  49. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
  50. package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
  51. package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
  52. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
  53. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
  54. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
  55. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
  56. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
  57. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
  58. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
  59. package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
  60. package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
  61. package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
  62. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
  63. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
  64. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
  65. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
  66. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
  67. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
  68. package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
  69. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
  70. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
  71. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
  72. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
  73. package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
  74. package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
  75. package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
  76. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
  77. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
  78. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
  79. package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
  80. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
  81. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
  82. package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
  83. package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
  84. package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
  85. package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
  86. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
  87. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
  88. package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
  89. package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
  90. package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
  91. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
  92. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
  93. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
  94. package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
  95. package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
  96. package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
  97. package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
  98. package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
  99. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
  100. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
  101. package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
  102. package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
  103. package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
  104. package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
  105. package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
  106. package/skill/assets/upstream/apps/desktop/package.json +51 -0
  107. package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
  108. package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
  109. package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
  110. package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
  111. package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
  112. package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
  113. package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
  114. package/skill/assets/upstream/packages/openfilm/package.json +88 -0
  115. package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
  116. package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
  117. package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
  118. package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
  119. package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
  120. package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
  121. package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
  122. package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
  123. package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
  124. package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
  125. package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
  126. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
  127. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
  128. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
  129. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
  130. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
  131. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
  132. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
  133. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
  134. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
  135. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
  136. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
  137. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
  138. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
  139. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
  140. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
  141. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
  142. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
  143. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
  144. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
  145. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
  146. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
  147. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
  148. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
  149. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
  150. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
  151. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
  152. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
  153. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
  154. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
  155. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
  156. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
  157. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
  158. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
  159. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
  160. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
  161. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
  162. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
  163. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
  164. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
  165. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
  166. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
  167. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
  168. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
  169. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
  170. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
  171. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
  172. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
  173. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
  174. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
  175. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
  176. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
  177. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
  178. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
  179. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
  180. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
  181. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
  182. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
  183. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
  184. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
  185. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
  186. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
  187. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
  188. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
  189. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
  190. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
  191. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
  192. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
  193. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
  194. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
  195. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
  196. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
  197. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
  198. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
  199. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
  200. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
  201. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
  202. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
  203. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
  204. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
  205. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
  206. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
  207. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
  208. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
  209. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
  210. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
  211. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
  212. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
  213. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
  214. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
  215. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
  216. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
  217. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
  218. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
  219. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
  220. package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
  221. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
  222. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
  223. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
  224. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
  225. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
  226. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
  227. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
  228. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
  229. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
  230. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
  231. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
  232. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
  233. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
  234. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
  235. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
  236. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
  237. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
  238. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
  239. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
  240. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
  241. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
  242. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
  243. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
  244. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
  245. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
  246. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
  247. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
  248. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
  249. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
  250. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
  251. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
  252. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
  253. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
  254. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
  255. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
  256. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
  257. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
  258. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
  259. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
  260. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
  261. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
  262. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
  263. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
  264. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
  265. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
  266. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
  267. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
  268. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
  269. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
  270. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
  271. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
  272. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
  273. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
  274. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
  275. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
  276. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
  277. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
  278. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
  279. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
  280. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
  281. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
  282. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
  283. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
  284. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
  285. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
  286. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
  287. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
  288. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
  289. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
  290. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
  291. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
  292. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
  293. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
  294. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
  295. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
  296. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
  297. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
  298. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
  299. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
  300. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
  301. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
  302. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
  303. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
  304. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
  305. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
  306. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
  307. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
  308. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
  309. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
  310. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
  311. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
  312. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
  313. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
  314. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
  315. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
  316. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
  317. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
  318. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
  319. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
  320. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
  321. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
  322. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
  323. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
  324. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
  325. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
  326. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
  327. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
  328. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
  329. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
  330. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
  331. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
  332. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
  333. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
  334. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
  335. package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
  336. package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
  337. package/skill/assets/upstream/packages/shared/package.json +45 -0
  338. package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
  339. package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
  340. package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
  341. package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
  342. package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
  343. package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
  344. package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
  345. package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
  346. package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
  347. package/skill/references/agent-components.md +33 -0
  348. package/skill/references/api-index.json +2693 -0
  349. package/skill/references/chat-components.md +65 -0
  350. package/skill/references/components.md +23 -0
  351. package/skill/references/core-components.md +44 -0
  352. package/skill/references/dependencies.md +22 -0
  353. package/skill/references/esm-package.md +48 -0
  354. package/skill/references/integration.md +43 -0
  355. package/skill/references/library-manifest.json +12 -0
  356. package/skill/references/source-manifest.json +325 -0
  357. package/skill/references/studio-components.md +24 -0
  358. package/skill/references/theme-motion.md +49 -0
  359. package/skill/references/troubleshooting.md +27 -0
  360. package/skill/references/verification.md +13 -0
  361. package/skill/scripts/check_dependencies.py +109 -0
  362. package/skill/scripts/inspect_api.py +60 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A project's conversations (the session bar's list), kept with the project (lib/chat-store.ts: its .film/chat), in
3
+ * the shape of lib/project-sessions.ts; the turns of each are kept beside them.
4
+ */
5
+ import type { ProjectSessionItem } from '@/lib/project-sessions';
6
+ import { chatSessions, lastSession, setChatSessions, setLastSession } from '@/lib/chat-store';
7
+
8
+ const MAX = 200;
9
+
10
+ export function loadSessions(project: string): ProjectSessionItem[] {
11
+ return chatSessions(project).filter((s): s is ProjectSessionItem => Boolean(s && typeof s.id === 'string' && typeof s.createdAt === 'string'));
12
+ }
13
+
14
+ export function saveSessions(project: string, sessions: ProjectSessionItem[]): void {
15
+ setChatSessions(project, sessions.slice(0, MAX));
16
+ }
17
+
18
+ export function newSession(): ProjectSessionItem {
19
+ const now = new Date().toISOString();
20
+ return { id: `s${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`, title: '', createdAt: now, updatedAt: now, turns: 0 };
21
+ }
22
+
23
+ /** The one shown last in this project, remembered across launches. */
24
+ export const loadLastSession = (project: string): string | null => lastSession(project);
25
+ export const saveLastSession = (project: string, id: string): void => setLastSession(project, id);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The requests an agent made to its model in a turn and the answers (the turn's model log, src/chat-store.mjs), whole
3
+ * again: a line keeps only what is new since the request before (`prefix` of that one's messages, then its own;
4
+ * `systemPrompt`, `tools`, `settings`, `source` and `thread` when they changed). The app's own agent (Pi) writes its
5
+ * own; the other agents' are read from their own records (src/agent-records.mjs), or from the logs they are started
6
+ * with in Developer mode (src/agent-logs.mjs).
7
+ *
8
+ * Messages are Pi's: system (its text in `sections`), user (text or parts), assistant (`thinking`, `text` and
9
+ * `toolCall` parts, its usage and why it stopped) and toolResult (`toolCallId`, `toolName`, parts, `isError`); from an
10
+ * agent's record also `context`: what the agent put in by itself (`label` says what, `as` the role it went in as).
11
+ */
12
+ type Item = Record<string, any>;
13
+
14
+ export interface RawCall {
15
+ startedAt: number;
16
+ /** null: the log does not say how long it took (Qwen Code's) */
17
+ ms: number | null;
18
+ prefix: number;
19
+ messages: Item[];
20
+ systemPrompt?: string | null;
21
+ tools?: Item[] | null;
22
+ /** Codex keeps no tool definitions: `tools` is not known, not empty */
23
+ toolsUnknown?: boolean;
24
+ /** the turn's settings as the agent kept them (Codex: model, effort, permissions) */
25
+ settings?: Item | null;
26
+ /** read from this agent's own record */
27
+ source?: { agent: string; file: string } | null;
28
+ /** a subagent's request (Claude Code), named */
29
+ thread?: { id: string; label: string } | null;
30
+ /** what the log this was read from does not have, by name (the turn log says each: turnLog.lacks.<name>) */
31
+ lacks?: string[];
32
+ answer: Item | null;
33
+ }
34
+
35
+ export interface ModelCall {
36
+ no: number;
37
+ startedAt: number;
38
+ ms: number | null;
39
+ systemPrompt: string | null;
40
+ /** every message the model got, in order; from `fresh` on, the ones new in this request */
41
+ messages: Item[];
42
+ fresh: number;
43
+ tools: Item[] | null;
44
+ toolsUnknown: boolean;
45
+ settings: Item | null;
46
+ source: { agent: string; file: string } | null;
47
+ thread: { id: string; label: string } | null;
48
+ lacks: string[];
49
+ answer: Item | null;
50
+ }
51
+
52
+ export function modelCalls(raw: RawCall[]): ModelCall[] {
53
+ let messages: Item[] = [];
54
+ let systemPrompt: string | null = null;
55
+ let tools: Item[] | null = null;
56
+ let settings: Item | null = null;
57
+ let source: ModelCall['source'] = null;
58
+ let thread: ModelCall['thread'] = null;
59
+ return raw.map((call, i) => {
60
+ messages = [...messages.slice(0, call.prefix), ...(call.messages ?? [])];
61
+ if ('systemPrompt' in call) systemPrompt = call.systemPrompt ?? null;
62
+ if ('tools' in call) tools = call.tools ?? null;
63
+ if ('settings' in call) settings = call.settings ?? null;
64
+ if ('source' in call) source = call.source ?? null;
65
+ if ('thread' in call) thread = call.thread ?? null;
66
+ return { no: i + 1, startedAt: call.startedAt, ms: call.ms, systemPrompt, messages, fresh: call.prefix, tools, toolsUnknown: call.toolsUnknown === true, settings, source, thread, lacks: call.lacks ?? [], answer: call.answer };
67
+ });
68
+ }
69
+
70
+ /** What an answer used: tokens in (of them read from the cache) and out (of them thinking); never a price. */
71
+ export function usageOf(call: ModelCall) {
72
+ const u = call.answer?.usage ?? {};
73
+ const cached = Number(u.cacheRead) || 0;
74
+ return {
75
+ input: (Number(u.input) || 0) + cached + (Number(u.cacheWrite) || 0),
76
+ cached,
77
+ output: Number(u.output) || 0,
78
+ reasoning: Number(u.reasoning) || 0,
79
+ };
80
+ }
81
+
82
+ /** A message's text: a system message's sections, or its text parts. */
83
+ export function textOf(message: Item): string {
84
+ if (message.role === 'system') return typeof message.content === 'string' && message.content ? message.content : Object.values(message.sections ?? {}).join('\n\n');
85
+ if (typeof message.content === 'string') return message.content;
86
+ return (message.content ?? []).map((p: Item) => (p.type === 'text' ? p.text : p.type === 'thinking' ? p.thinking : p.type === 'toolCall' ? `${p.name} ${JSON.stringify(p.arguments)}` : p.type === 'image' ? '[image]' : '')).join(' ');
87
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compact JSON on disk is one line. Viewers expand it so a file is readable
3
+ * without rewriting what the author (or agent) saved.
4
+ */
5
+ export function prettyJsonSource(path: string, raw: string): string {
6
+ if (!/\.json$/i.test(path) || /\.jsonl$/i.test(path)) return raw;
7
+ try {
8
+ return `${JSON.stringify(JSON.parse(raw), null, 2)}\n`;
9
+ } catch {
10
+ return raw;
11
+ }
12
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Where the lightbox (PreviewFrame) starts at the top of the window. On Windows the window's own buttons are drawn
3
+ * over the page's top right (src/main.mjs, `titleBarOverlay`), above anything the page draws: the lightbox starts
4
+ * below them, so its close button is never under the window's, which quits OpenFilm.
5
+ *
6
+ * Pure: no React, no bridge, so `node --test` runs it (src/preview-top.test.mjs).
7
+ */
8
+
9
+ /** How tall Windows draws the window's buttons over the page: `titleBarOverlay`'s height in src/main.mjs. */
10
+ export const WINDOW_BUTTONS_H = 32;
11
+
12
+ /** The lightbox's top padding, as CSS, on this platform (`navigator.platform`). */
13
+ export function previewTop(platform: string): string {
14
+ return platform.startsWith('Win') ? `${WINDOW_BUTTONS_H + 8}px` : 'max(1rem, var(--inset-titlebar-h, 0px))';
15
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Which kind a file is, and its icon, so a file looks the same everywhere in the chat.
3
+ */
4
+ import {
5
+ File as FileIcon,
6
+ FileArchive,
7
+ FileAudio,
8
+ FileCode,
9
+ FileJson,
10
+ FileSpreadsheet,
11
+ FileText,
12
+ FileVideo,
13
+ Folder,
14
+ Image as ImageIcon,
15
+ Subtitles,
16
+ type LucideIcon,
17
+ } from 'lucide-react';
18
+ import { AUDIO_UPLOAD_EXTENSIONS, DOCUMENT_EXTENSIONS, IMAGE_EXTENSIONS, VIDEO_UPLOAD_EXTENSIONS } from '@/lib/upload-limits';
19
+
20
+ export type WorkspaceResourceKind = 'video' | 'image' | 'audio' | 'document' | 'mg' | 'other';
21
+
22
+ export const RESOURCE_KIND_ICON: Record<WorkspaceResourceKind, LucideIcon> = {
23
+ video: FileVideo,
24
+ image: ImageIcon,
25
+ audio: FileAudio,
26
+ document: FileText,
27
+ mg: FileVideo,
28
+ other: FileIcon,
29
+ };
30
+
31
+ const EXTENSION_ICON: Record<string, LucideIcon> = {
32
+ json: FileJson, csv: FileSpreadsheet, xls: FileSpreadsheet, xlsx: FileSpreadsheet, srt: Subtitles, vtt: Subtitles,
33
+ zip: FileArchive, rar: FileArchive, '7z': FileArchive, tar: FileArchive, gz: FileArchive,
34
+ js: FileCode, ts: FileCode, tsx: FileCode, html: FileCode, css: FileCode, py: FileCode,
35
+ };
36
+
37
+ /** A file's extension (no dot, lower case); empty when it has none. */
38
+ export function fileExtension(name: string): string {
39
+ const cut = name.lastIndexOf('.');
40
+ return cut <= 0 ? '' : name.slice(cut + 1).toLowerCase();
41
+ }
42
+
43
+ export function resourceIcon(file: { name: string; kind: string }): LucideIcon {
44
+ return EXTENSION_ICON[fileExtension(file.name)] ?? RESOURCE_KIND_ICON[file.kind as WorkspaceResourceKind] ?? RESOURCE_KIND_ICON.other;
45
+ }
46
+
47
+ /** Which kind a file is, by its extension first (Finder often reports a .mov as octet-stream), then its type. */
48
+ export function attachmentKind(file: { name: string; mediaType?: string }): WorkspaceResourceKind {
49
+ const ext = fileExtension(file.name);
50
+ if (VIDEO_UPLOAD_EXTENSIONS.has(ext)) return 'video';
51
+ if (AUDIO_UPLOAD_EXTENSIONS.has(ext)) return 'audio';
52
+ if ((IMAGE_EXTENSIONS as readonly string[]).includes(ext)) return 'image';
53
+ if (DOCUMENT_EXTENSIONS.has(ext)) return 'document';
54
+ const mime = (file.mediaType ?? '').toLowerCase().split(';')[0]!.trim();
55
+ if (mime.startsWith('video/')) return 'video';
56
+ if (mime.startsWith('audio/')) return 'audio';
57
+ if (mime.startsWith('image/')) return 'image';
58
+ if (mime === 'application/pdf' || mime.startsWith('text/')) return 'document';
59
+ return 'other';
60
+ }
61
+
62
+ /** The icon for a file in the composer or a message; a folder's name ends in "/" (desktop-attachments turnAttachments). */
63
+ export function attachmentIcon(file: { name: string; mediaType?: string; folder?: boolean }): LucideIcon {
64
+ if (file.folder || file.name.endsWith('/')) return Folder;
65
+ return resourceIcon({ name: file.name, kind: attachmentKind(file) });
66
+ }
@@ -0,0 +1,66 @@
1
+ import type { SkillsGiven } from './skill-pages';
2
+
3
+ /**
4
+ * A project's sessions: their shape on the client, and how the session bar groups them by when they were last used.
5
+ */
6
+
7
+ export interface ProjectSessionItem {
8
+ id: string;
9
+ title: string;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ /** How many turns the session has. 0: just opened, nothing said yet. */
13
+ turns: number;
14
+ /** Each agent's own id of this conversation (by agent): its next turn goes on with it, after a restart too. */
15
+ agentSessions?: Record<string, string>;
16
+ /** The skills whose SKILL.md each agent's session has had in this conversation (lib/skill-pages), by agent. */
17
+ skillsGiven?: SkillsGiven;
18
+ }
19
+
20
+ export type SessionRecencyKey = 'today' | 'yesterday' | 'week' | 'earlier';
21
+
22
+ export interface SessionRecencyGroup {
23
+ key: SessionRecencyKey;
24
+ items: ProjectSessionItem[];
25
+ }
26
+
27
+ function startOfLocalDay(ts: number): number {
28
+ const date = new Date(ts);
29
+ date.setHours(0, 0, 0, 0);
30
+ return date.getTime();
31
+ }
32
+
33
+ /**
34
+ * Groups into today, yesterday, the past 7 days and earlier, keeping the given order within each group.
35
+ *
36
+ * By updatedAt (else createdAt): a session talked in recently belongs under today, not the day it was created.
37
+ */
38
+ export function groupSessionsByRecency(
39
+ items: readonly ProjectSessionItem[],
40
+ now = Date.now(),
41
+ ): SessionRecencyGroup[] {
42
+ const today = startOfLocalDay(now);
43
+ const yesterday = today - 86_400_000;
44
+ const week = today - 7 * 86_400_000;
45
+ const buckets: Record<SessionRecencyKey, ProjectSessionItem[]> = {
46
+ today: [],
47
+ yesterday: [],
48
+ week: [],
49
+ earlier: [],
50
+ };
51
+ for (const item of items) {
52
+ const at = Date.parse(item.updatedAt || item.createdAt);
53
+ const key: SessionRecencyKey = Number.isNaN(at) || at >= today
54
+ ? 'today'
55
+ : at >= yesterday
56
+ ? 'yesterday'
57
+ : at >= week
58
+ ? 'week'
59
+ : 'earlier';
60
+ buckets[key].push(item);
61
+ }
62
+ const order: SessionRecencyKey[] = ['today', 'yesterday', 'week', 'earlier'];
63
+ return order
64
+ .filter((key) => buckets[key].length > 0)
65
+ .map((key) => ({ key, items: buckets[key] }));
66
+ }
@@ -0,0 +1,116 @@
1
+ import type { Turn, LibraryVideo, PromptDisplay, TurnAttachment } from '@openfilm/shared';
2
+ import { readFileChanges, type FileChange, type TurnFileChanges } from './file-changes.ts';
3
+ import { readBalance, type Balance, type TurnBalance } from './balance.ts';
4
+
5
+ /**
6
+ * A project's conversation: each of its turns, in time order.
7
+ *
8
+ * Not every turn makes a film: a turn can be only talk, or stop before a film. So the list is every turn of the
9
+ * conversation; a film only decides whether a row has a poster and counts as a version, not whether it exists.
10
+ */
11
+ export interface ProjectVersionEntry {
12
+ assetId: string;
13
+ /** Turns that made a film get a version (v1..vN); a turn that only talked has none. */
14
+ label: string;
15
+ title: string;
16
+ /** What the person sent in this turn; the content of its bubble. */
17
+ prompt: string;
18
+ /**
19
+ * The message as written in the composer (with pill markers). When present it is rendered, pills as pills.
20
+ *
21
+ * Empty in two cases, both falling back to `prompt`: the message referenced no pill (the two are identical),
22
+ * or it was sent before this field existed.
23
+ */
24
+ promptDisplay?: PromptDisplay;
25
+ /** What came with the message (names and thumbnails); empty when nothing did. */
26
+ attachments: TurnAttachment[];
27
+ /** The poster of the film this turn made; empty when it made none (talk, running, failed). */
28
+ posterUrl?: string;
29
+ status: 'ready' | 'active' | 'failed';
30
+ current: boolean;
31
+ /** Which agent answered (codex, claude…). */
32
+ agent?: string;
33
+ /** The model name shown, e.g. "gpt-5.5 · Medium". */
34
+ model?: string;
35
+ /** How long the turn ran. Only for settled turns whose times make sense. */
36
+ durationMs?: number;
37
+ /** How the turn stopped when it didn't finish normally. Empty when it did. */
38
+ outcome?: 'stopped' | 'failed' | 'balance';
39
+ /** outcome 'balance': which service had no balance left, and its page */
40
+ balance?: Balance;
41
+ /** outcome 'failed': why, as the turn kept it */
42
+ error?: string;
43
+ /** When the message was sent (ISO). Hovering under the bubble says "N minutes ago". */
44
+ createdAt?: string;
45
+ /** When the turn settled (ISO), only for settled turns. Hovering under the reply says "N minutes ago". */
46
+ settledAt?: string;
47
+ /** What the turn changed in the project's files (lib/file-changes); none when nothing did. */
48
+ fileChanges?: FileChange[];
49
+ /** 'background': the agent started it by itself, a background task of its own having come back (no message) */
50
+ origin?: 'background';
51
+ }
52
+
53
+ /** Longest believable turn: anything longer most likely means a later write touched updatedAt. */
54
+ const DURATION_MAX_MS = 3 * 60 * 60 * 1000;
55
+
56
+ function frameOf(asset: Turn): Pick<ProjectVersionEntry, 'agent' | 'model' | 'durationMs' | 'outcome' | 'balance' | 'error' | 'createdAt' | 'settledAt'> {
57
+ const settled = asset.status === 'ready' || asset.status === 'error' || asset.status === 'cancelled';
58
+ const ms = Date.parse(asset.updatedAt) - Date.parse(asset.createdAt);
59
+ const model = asset.localAgent?.model ?? null;
60
+ /* no balance left: said by the service's name, the same for every one */
61
+ const balance = asset.status === 'error' ? readBalance((asset as Turn & TurnBalance).balance) : null;
62
+ const outcome = asset.status === 'cancelled' ? 'stopped' as const
63
+ : balance ? 'balance' as const : asset.status === 'error' ? 'failed' as const : null;
64
+ /* a turn saved before turns named their agent: which one is not known */
65
+ const agent = asset.localAgent?.id ?? null;
66
+ return {
67
+ ...(agent ? { agent } : {}),
68
+ ...(model ? { model } : {}),
69
+ ...(settled && Number.isFinite(ms) && ms > 0 && ms < DURATION_MAX_MS ? { durationMs: ms } : {}),
70
+ ...(outcome ? { outcome } : {}),
71
+ ...(balance ? { balance } : {}),
72
+ ...(outcome === 'failed' && asset.error ? { error: asset.error } : {}),
73
+ ...(asset.createdAt ? { createdAt: asset.createdAt } : {}),
74
+ ...(settled && asset.updatedAt ? { settledAt: asset.updatedAt } : {}),
75
+ };
76
+ }
77
+
78
+ function statusOf(asset: Turn, hasFilm: boolean): ProjectVersionEntry['status'] {
79
+ if (asset.status === 'generating' || asset.status === 'failing' || asset.status === 'canceling') {
80
+ return 'active';
81
+ }
82
+ /* Ready without a film means the turn only talked; it didn't fail. Marked failed, a simple question like
83
+ "how long is this film" would get a red label. */
84
+ if (asset.status === 'ready') return 'ready';
85
+ return hasFilm ? 'ready' : 'failed';
86
+ }
87
+
88
+ export function deriveProjectTurns(
89
+ turns: Turn[],
90
+ videos: LibraryVideo[],
91
+ selectedId: string | null,
92
+ ): ProjectVersionEntry[] {
93
+ /* The order is set here: callers pass lists from mergeAssetLists, newest first, which would render the
94
+ conversation backwards. */
95
+ const ordered = [...turns].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
96
+ let filmCount = 0;
97
+ return ordered.map((asset) => {
98
+ const film = videos.find((item) => item.turnId === asset.id);
99
+ if (film) filmCount += 1;
100
+ const fileChanges = readFileChanges((asset as Turn & TurnFileChanges).fileChanges);
101
+ return {
102
+ assetId: asset.id,
103
+ label: film ? `v${filmCount}` : '',
104
+ title: asset.prompt,
105
+ prompt: asset.prompt,
106
+ ...(asset.promptDisplay ? { promptDisplay: asset.promptDisplay } : {}),
107
+ attachments: asset.attachments ?? [],
108
+ ...(film?.result.posterUrl ? { posterUrl: film.result.posterUrl } : {}),
109
+ status: statusOf(asset, Boolean(film)),
110
+ current: asset.id === selectedId,
111
+ ...frameOf(asset),
112
+ ...(fileChanges ? { fileChanges } : {}),
113
+ ...((asset as Turn & { origin?: unknown }).origin === 'background' ? { origin: 'background' as const } : {}),
114
+ };
115
+ });
116
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The pictures of a message's references, for its agent: each the frame Studio gave the reference (only the part
3
+ * pointed at, for a region or a layer), as a JPEG kept in the project (.film/refs/<turn>-<n>.jpg, which the message
4
+ * names) and, for an agent that takes pictures, sent with the message.
5
+ */
6
+ import { app, type StudioRef, type StudioRefImage } from '@/app-bridge';
7
+ import type { PromptImage } from '@/lib/desktop-bridge';
8
+
9
+ /** at most this many pictures a message, each at most this wide: enough to see, little enough to send */
10
+ const MAX_PICTURES = 8;
11
+ const MAX_WIDTH = 1280;
12
+ /** a frame Studio has to draw first can take a moment; one that takes longer is left out */
13
+ const LOAD_TIMEOUT_MS = 15_000;
14
+
15
+ /** The reference's picture as a JPEG: fetched from Studio, cut to its crop (stage px, scaled to the picture's). */
16
+ async function encode(image: StudioRefImage): Promise<Blob> {
17
+ if (!image.src) throw new Error('no picture was taken');
18
+ const res = await fetch(image.src, { signal: AbortSignal.timeout(LOAD_TIMEOUT_MS) });
19
+ if (!res.ok) throw new Error(`Studio answered ${res.status}`);
20
+ const bitmap = await createImageBitmap(await res.blob());
21
+ try {
22
+ let sx = 0;
23
+ let sy = 0;
24
+ let sw = bitmap.width;
25
+ let sh = bitmap.height;
26
+ if (image.crop && image.stage && image.stage.w > 0) {
27
+ const scale = bitmap.width / image.stage.w;
28
+ sx = Math.max(0, Math.round(image.crop.x * scale));
29
+ sy = Math.max(0, Math.round(image.crop.y * scale));
30
+ sw = Math.min(bitmap.width - sx, Math.round(image.crop.w * scale));
31
+ sh = Math.min(bitmap.height - sy, Math.round(image.crop.h * scale));
32
+ }
33
+ if (sw < 1 || sh < 1) throw new Error('nothing to cut');
34
+ const out = Math.min(1, MAX_WIDTH / sw);
35
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(sw * out)), Math.max(1, Math.round(sh * out)));
36
+ const context = canvas.getContext('2d');
37
+ if (!context) throw new Error('no canvas');
38
+ /* a JPEG has no transparency: a page's see-through parts read as white, not black */
39
+ context.fillStyle = '#fff';
40
+ context.fillRect(0, 0, canvas.width, canvas.height);
41
+ context.drawImage(bitmap, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
42
+ return await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.85 });
43
+ } finally {
44
+ bitmap.close();
45
+ }
46
+ }
47
+
48
+ function base64(bytes: ArrayBuffer): string {
49
+ const view = new Uint8Array(bytes);
50
+ let text = '';
51
+ for (let i = 0; i < view.length; i += 0x8000) text += String.fromCharCode(...view.subarray(i, i + 0x8000));
52
+ return btoa(text);
53
+ }
54
+
55
+ /**
56
+ * Make the pictures of `refs` (in their order, numbered from 1 as in the message): `pictures[i]` is where reference
57
+ * [i + 1]'s was kept in the project (none when it has none, or it did not load or could not be kept), `images` what
58
+ * the agent is sent. Never throws: a picture that fails only leaves its line without one.
59
+ */
60
+ export async function referencePictures(refs: StudioRef[], { project, turnId }: { project: string; turnId: string }): Promise<{
61
+ pictures: Array<string | undefined>;
62
+ images: PromptImage[];
63
+ }> {
64
+ const wanted = refs.flatMap((ref, i) => (ref.image ? [{ n: i + 1, image: ref.image }] : [])).slice(0, MAX_PICTURES);
65
+ const made = await Promise.all(wanted.map(async ({ n, image }) => {
66
+ try {
67
+ const bytes = await (await encode(image)).arrayBuffer();
68
+ const kept = await app.chat.saveRefImage(project, turnId, n, bytes).then((r) => r.path, () => undefined);
69
+ return { n, path: kept, image: { data: base64(bytes), mimeType: 'image/jpeg' } };
70
+ } catch {
71
+ return null;
72
+ }
73
+ }));
74
+ const pictures: Array<string | undefined> = refs.map(() => undefined);
75
+ const images: PromptImage[] = [];
76
+ for (const picture of made) {
77
+ if (!picture) continue;
78
+ pictures[picture.n - 1] = picture.path;
79
+ images.push(picture.image);
80
+ }
81
+ return { pictures, images };
82
+ }
83
+
84
+ /**
85
+ * an attached picture is sent as it is up to this size and this many pixels on its long side; larger, made smaller.
86
+ * 2000: a request with more than 20 pictures in it (a conversation's, not only this message's) takes none larger
87
+ */
88
+ const ATTACHED_MAX_BYTES = 4 * 1024 * 1024;
89
+ const ATTACHED_MAX_SIDE = 2000;
90
+ const AS_IS = new Set(['image/jpeg', 'image/png', 'image/webp']);
91
+
92
+ /**
93
+ * A picture attached to a message, as its agent is sent it beside the text (as a picture pasted in its own chat is):
94
+ * itself when it is a JPEG, PNG or WebP small enough, else a JPEG at most ATTACHED_MAX_SIDE on its long side. Null
95
+ * when it cannot be read as a picture.
96
+ */
97
+ export async function attachedPicture(file: Blob): Promise<PromptImage | null> {
98
+ try {
99
+ const bitmap = await createImageBitmap(file);
100
+ try {
101
+ const long = Math.max(bitmap.width, bitmap.height);
102
+ if (AS_IS.has(file.type) && file.size <= ATTACHED_MAX_BYTES && long <= ATTACHED_MAX_SIDE) {
103
+ return { data: base64(await file.arrayBuffer()), mimeType: file.type };
104
+ }
105
+ const scale = Math.min(1, ATTACHED_MAX_SIDE / long);
106
+ const canvas = new OffscreenCanvas(Math.max(1, Math.round(bitmap.width * scale)), Math.max(1, Math.round(bitmap.height * scale)));
107
+ const context = canvas.getContext('2d');
108
+ if (!context) return null;
109
+ context.fillStyle = '#fff';
110
+ context.fillRect(0, 0, canvas.width, canvas.height);
111
+ context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
112
+ return { data: base64(await (await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.85 })).arrayBuffer()), mimeType: 'image/jpeg' };
113
+ } finally {
114
+ bitmap.close();
115
+ }
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * "38 minutes ago": how long ago a message or reply was (shown under it on hover).
5
+ *
6
+ * Intl.RelativeTimeFormat in the UI language, no strings of our own. Beyond a week it gives the date: "43 days
7
+ * ago" makes the reader do the arithmetic.
8
+ *
9
+ * One minute tick for the whole page (useMinuteTick): a long conversation has dozens of times, and a timer each
10
+ * would wake dozens of components every minute.
11
+ */
12
+ import * as React from 'react';
13
+
14
+ export function relativeTime(iso: string | undefined, locale: string, now: number): string {
15
+ const at = iso ? Date.parse(iso) : NaN;
16
+ if (!Number.isFinite(at) || !now) return '';
17
+ /* `now` moves once a minute (useMinuteTick), so a line from the last few seconds can be up to a minute
18
+ "ahead" of it: that is just now, never "in 1 minute". */
19
+ const sec = Math.min(0, Math.round((at - now) / 1000));
20
+ const fmt = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
21
+ const abs = Math.abs(sec);
22
+ if (abs < 45) return fmt.format(0, 'second');
23
+ if (abs < 3600) return fmt.format(Math.round(sec / 60), 'minute');
24
+ if (abs < 86_400) return fmt.format(Math.round(sec / 3600), 'hour');
25
+ if (abs < 7 * 86_400) return fmt.format(Math.round(sec / 86_400), 'day');
26
+ return new Date(at).toLocaleDateString(locale, { month: 'short', day: 'numeric', ...(new Date(at).getFullYear() !== new Date(now).getFullYear() ? { year: 'numeric' } : {}) });
27
+ }
28
+
29
+ const listeners = new Set<() => void>();
30
+ let minute = Date.now();
31
+ let timer: number | null = null;
32
+ function subscribe(fn: () => void): () => void {
33
+ listeners.add(fn);
34
+ if (timer == null) { minute = Date.now(); timer = window.setInterval(() => { minute = Date.now(); listeners.forEach((l) => l()); }, 60_000); }
35
+ return () => {
36
+ listeners.delete(fn);
37
+ if (!listeners.size && timer != null) { window.clearInterval(timer); timer = null; }
38
+ };
39
+ }
40
+ const snapshot = () => minute;
41
+ const serverSnapshot = () => 0;
42
+
43
+ /** Now, updated once a minute. */
44
+ export function useMinuteTick(): number {
45
+ return React.useSyncExternalStore(subscribe, snapshot, serverSnapshot);
46
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A skill named in a message (its pill) is for that message. Its SKILL.md goes to the agent with it, unseen in the
3
+ * chat, once per conversation: the agent keeps it in its own session from then on, so naming the skill again sends only
4
+ * its name. What each agent's session was given is kept with the conversation (`skillsGiven`, by agent); a session the
5
+ * agent could not take up again starts with nothing. Reads nothing, so `node --test` can use it.
6
+ */
7
+
8
+ /** Per agent: its own session id (null before it has one) and the skills whose pages that session was given. */
9
+ export type SkillsGiven = Record<string, { session: string | null; names: string[] }>;
10
+
11
+ /** The skills named in this message whose pages go with it: those `agent`'s current `session` has not had. */
12
+ export function pagesToSend(named: readonly string[], given: Readonly<SkillsGiven> | undefined, agent: string, session: string | null): string[] {
13
+ const had = given?.[agent];
14
+ const known = new Set(had && had.session === session ? had.names : []);
15
+ return [...new Set(named)].filter((name) => !known.has(name));
16
+ }
17
+
18
+ /**
19
+ * What `agent` has been given once a message goes with the pages of `sent`. `session`: its session the message went to;
20
+ * `fresh`: one just started that has nothing from before (the pages it holds are those of this message only).
21
+ */
22
+ export function givenAfter(given: Readonly<SkillsGiven> | undefined, agent: string, session: string | null, sent: readonly string[], { fresh = false } = {}): SkillsGiven {
23
+ const out: SkillsGiven = {};
24
+ for (const [by, had] of Object.entries(given ?? {})) out[by] = { session: had.session, names: [...had.names] };
25
+ const had = out[agent];
26
+ const before = !fresh && had && had.session === session ? had.names : [];
27
+ out[agent] = { session, names: [...new Set([...before, ...sent])] };
28
+ return out;
29
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The file a step of the user's own agent names in its row (lib/local-agent-run localToolActivity): the base name of
3
+ * its path, with either separator.
4
+ *
5
+ * The path is where ACP puts it, in the call's locations or its input (`file_path`, `path`, `filePath`), and else
6
+ * in its title. A read's last update often carries only its title, or nothing but what was read, and each agent
7
+ * titles a read its own way: Claude Code "Read .film\cache\look\0-5s.png (1 - 40)", OpenCode the path without its
8
+ * drive ("Users\me\…\0-5s.png"), Copilot "Viewing …\look\0-5s.png", Cursor "Read C:\…\SKILL.md". A title that is
9
+ * no path (Claude Code's "Read File" before it knows which, OpenCode's "read") names no file, so the row keeps the
10
+ * one it has.
11
+ *
12
+ * Pure: no React, no bridge, so `node --test` runs it (src/step-file.test.mjs).
13
+ */
14
+
15
+ export function baseName(path: string): string {
16
+ return path.replace(/[\\/]+$/, '').split(/[\\/]/).pop() ?? path;
17
+ }
18
+
19
+ /** The file a title names when it is a path, after its verb and line range; undefined when it is none. */
20
+ export function titleFile(title: string): string | undefined {
21
+ const path = title.trim().replace(/^(?:Read|View Image|Viewing)\s+/i, '').replace(/\s+\((?:from line \d+|\d+ - \d+)\)$/, '');
22
+ return /[\\/]|\.\w+$/.test(path) ? baseName(path) : undefined;
23
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Structural sharing: subtrees of `next` deeply equal to `prev` get `prev`'s references back; if all is equal,
3
+ * `prev` itself is returned.
4
+ *
5
+ * Each poll returns freshly parsed objects even when nothing changed. Put straight into state, every card and turn
6
+ * gets new props and every memo misses, worse the longer the list. Through here, unchanged parts keep their
7
+ * references and React skips them; when nothing changed, setState gets the same reference and skips the render.
8
+ *
9
+ * Handles JSON shapes only (objects, arrays, primitives), which is what the API returns.
10
+ */
11
+ export function shareStructure<T>(prev: T, next: T): T {
12
+ if (Object.is(prev, next)) return prev;
13
+ if (typeof prev !== 'object' || typeof next !== 'object' || prev === null || next === null) {
14
+ return next;
15
+ }
16
+ if (Array.isArray(prev) !== Array.isArray(next)) return next;
17
+ if (Array.isArray(prev)) {
18
+ const a = prev as unknown[];
19
+ const b = next as unknown[];
20
+ let same = a.length === b.length;
21
+ const out = b.map((item, i) => {
22
+ const shared = i < a.length ? shareStructure(a[i], item) : item;
23
+ if (shared !== a[i]) same = false;
24
+ return shared;
25
+ });
26
+ return (same ? prev : out) as T;
27
+ }
28
+ const a = prev as Record<string, unknown>;
29
+ const b = next as Record<string, unknown>;
30
+ const keys = Object.keys(b);
31
+ let same = keys.length === Object.keys(a).length;
32
+ const out: Record<string, unknown> = {};
33
+ for (const k of keys) {
34
+ const shared = k in a ? shareStructure(a[k], b[k]) : b[k];
35
+ if (shared !== a[k] || !(k in a)) same = false;
36
+ out[k] = shared;
37
+ }
38
+ return (same ? prev : out) as T;
39
+ }
40
+
41
+ /** The list version, matched by id: each item shares structure with the previous item of the same id; unchanged, returns `prev` itself. */
42
+ export function shareListById<T extends { id: string }>(prev: readonly T[], next: T[]): T[] {
43
+ const byId = new Map(prev.map((item) => [item.id, item]));
44
+ let same = prev.length === next.length;
45
+ const out = next.map((item, i) => {
46
+ const old = byId.get(item.id);
47
+ const shared = old ? shareStructure(old, item) : item;
48
+ if (shared !== prev[i]) same = false;
49
+ return shared;
50
+ });
51
+ return same ? (prev as T[]) : out;
52
+ }