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,716 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ AudioLines, Captions, ChevronDown, CornerDownRight, ChevronRight, Circle, CircleCheck, CircleDot, Clapperboard, Ear, Eye, FilePen, FileSearch,
6
+ FileText, Globe, ImagePlus, Languages, Layers, MessageCircleQuestion, Mic, Music, Pause, PenLine, Play, Search, SquareTerminal,
7
+ UserRound, Video, type LucideIcon,
8
+ } from 'lucide-react';
9
+ import type { AgentActAsset } from '@openfilm/shared';
10
+ import { Markdown } from './Markdown';
11
+ import { AttachmentPreview } from './AttachmentPreview';
12
+ import { elapsedLabel, useTick } from './useTicker';
13
+ import { useT } from '@/i18n';
14
+ import { actGroupParts, buildRuns, EPHEMERAL_ACT_LABELS, idleStatus, pendingWork } from '@/lib/agent-activity-runs';
15
+ import { StickToBottomContext } from './useStickToBottom';
16
+ import { WaitingContext } from '@/lib/developer';
17
+
18
+ /**
19
+ * The process the user sees: what the agent says and what it does.
20
+ *
21
+ * This file and `AgentConversationRow` are two renderers, not one with a switch. That one draws raw tool
22
+ * rows (tool name, input, output, reasoning) for `/dev`; this one draws projected acts. The split is
23
+ * deliberate: no code on the product path can render a tool name or a file path, so keeping them out of
24
+ * sight does not depend on anyone guarding a switch.
25
+ *
26
+ * No expandable input / output here, no ✓/✗. One row is one plain sentence. Acts finished back to back
27
+ * fold into one row that opens on click, like Cursor's step summary.
28
+ */
29
+
30
+ export interface AgentActivityItem {
31
+ /** thought / plan only come from an agent on the user's machine (see TranscriptItem in agent-transcript). */
32
+ kind: 'text' | 'act' | 'notice' | 'thought' | 'plan';
33
+ /**
34
+ * i18n token.
35
+ * · act — `act.<name>`, a closed set (see AGENT_ACTS in @openfilm/shared)
36
+ * · notice — the note the platform leaves (empty for an agent's own notice: its words are in `text`)
37
+ * · text — unused; the body is in `text`
38
+ */
39
+ label: string;
40
+ /** Merge key; the React key when a group is opened. */
41
+ id?: string;
42
+ /** What the agent says (markdown). */
43
+ text?: string;
44
+ /** The short tail after the act: a search query or a picture description. Picked before it gets here; only shown. */
45
+ query?: string;
46
+ /** A local agent's file name or command (no quotes: it is not film content). */
47
+ detail?: string;
48
+ plan?: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>;
49
+ /** The question the agent asks the user (chat.ask). */
50
+ ask?: { question: string; options: Array<{ label: string; description?: string }>; answer?: string; status?: 'answered' | 'skipped' | 'timeout' };
51
+ /** The asset this step made. */
52
+ asset?: AgentActAsset;
53
+ turnNo?: number;
54
+ pending?: boolean;
55
+ /** When a running item started (live timer; live stream only, not archives). */
56
+ startedAt?: number;
57
+ endedAt?: number;
58
+ }
59
+
60
+ /** One folded run. Acts finished back to back form a group; text and the running row stand alone. */
61
+ interface ActivityRun {
62
+ items: AgentActivityItem[];
63
+ }
64
+
65
+ /**
66
+ * Row elements already built, keyed by row identity.
67
+ *
68
+ * `memo` stops the render function running again, not the element being created again, and while
69
+ * streaming the latter dominates: 300 rows in a turn means 300 `jsx()` calls per token, some 7,000 a
70
+ * second at twenty-odd tokens a second.
71
+ *
72
+ * Reusing the same element object also lets React take its earliest bail-out on
73
+ * `oldProps === newProps`, without comparing props.
74
+ *
75
+ * The inner map is bucketed by key because one row has different keys in different lists (live and
76
+ * archived). The fold count is in the key too: when a run of repeated acts grows, only that row is
77
+ * rebuilt, not the ones before it.
78
+ */
79
+ const ROW_ELEMENTS = new WeakMap<AgentActivityItem, Map<string, React.ReactElement>>();
80
+
81
+ function rowElement(run: ActivityRun, key: string): React.ReactElement {
82
+ const first = run.items[0]!;
83
+ let byKey = ROW_ELEMENTS.get(first);
84
+ if (!byKey) { byKey = new Map(); ROW_ELEMENTS.set(first, byKey); }
85
+ // An asset can arrive late (the saved transcript fills in a receipt the live stream missed) with the count unchanged, so the asset count is in the key too.
86
+ let assets = 0;
87
+ for (const item of run.items) if (item.asset) assets += 1;
88
+ const cacheKey = `${key}#${run.items.length}#${assets}`;
89
+ const hit = byKey.get(cacheKey);
90
+ if (hit) return hit;
91
+ const made = <AgentActivityRow key={key} items={run.items} />;
92
+ byKey.set(cacheKey, made);
93
+ return made;
94
+ }
95
+
96
+ function actCopyKey(label: string, pending?: boolean): string {
97
+ if (pending || EPHEMERAL_ACT_LABELS.has(label) || !label.startsWith('act.')) return label;
98
+ return `actDone.${label.slice(4)}`;
99
+ }
100
+
101
+ export const AgentActivityList = React.memo(function AgentActivityList({
102
+ items,
103
+ keyPrefix = 'row',
104
+ }: {
105
+ items: AgentActivityItem[];
106
+ keyPrefix?: string;
107
+ }) {
108
+ /* Folding depends only on the list. While streaming, every delta gives the list a new identity and this
109
+ loop reruns, but its nodes take row objects cached per item (see toTraceLogItems), so the memoized rows
110
+ below all bail out: a rerun costs one O(n) fold, not n renders. */
111
+ /* Agent text is always laid out as body text, running or finished. Whether a paragraph closes the turn is
112
+ only known once the next act arrives, so styling asides differently would make each paragraph flash and
113
+ jump. Answer and process are told apart by collapsing (turns you did not watch show only the answer). */
114
+ const nodes = React.useMemo(
115
+ () => {
116
+ const runs = buildRuns(items);
117
+ return runs.map((run, index) => rowElement(
118
+ run,
119
+ `${keyPrefix}-${run.items[0]?.id ?? index}`,
120
+ ));
121
+ },
122
+ [items, keyPrefix],
123
+ );
124
+ return <>{nodes}</>;
125
+ });
126
+
127
+ /**
128
+ * One row, or a folded group.
129
+ *
130
+ * `memo` is load-bearing here: a run has hundreds of rows and while streaming only the growing row
131
+ * changes. Every other row's `item` is the same object (toTraceLogItems caches by source identity), so
132
+ * they all skip re-rendering.
133
+ */
134
+ const AgentActivityRow = React.memo(function AgentActivityRow({
135
+ items,
136
+ }: {
137
+ items: AgentActivityItem[];
138
+ }) {
139
+ const t = useT();
140
+ const item = items[0];
141
+ if (!item) return null;
142
+
143
+ /* A note the platform leaves in the conversation (today only "earlier messages were summarized").
144
+ Drawn as a labeled divider: it marks that the agent no longer sees the original text above it. */
145
+ if (item.kind === 'notice') {
146
+ /* A local agent's own notice (a Codex warning): its words, quiet, apart from what it says to the person */
147
+ if (item.text) return <p className="break-words py-1 text-[12px] leading-relaxed text-[var(--chat-hint)]">{item.text}</p>;
148
+ return (
149
+ <div className="flex items-center gap-2.5 py-1.5">
150
+ <span className="h-px flex-1 bg-[var(--border)]" />
151
+ <span className="shrink-0 text-[11px] text-[var(--chat-hint)]">{t(item.label)}</span>
152
+ <span className="h-px flex-1 bg-[var(--border)]" />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ if (item.kind === 'text') {
158
+ /* Agent text is markdown. As plain text, `**bold**`, `###` and `>` would show raw and paragraphs would
159
+ need splitting by hand; block parsing also drops extra blank lines. */
160
+ const text = item.text ?? '';
161
+ if (!text.trim()) return null;
162
+ return (
163
+ <div className="py-1.5">
164
+ <Markdown text={text} />
165
+ </div>
166
+ );
167
+ }
168
+
169
+ if (item.kind === 'plan') return <PlanRow item={item} />;
170
+ /* A lone act that made something folds too: its player would otherwise vanish the moment a second act
171
+ joins the run and the row turns into a closed group (a live row may change, never disappear). */
172
+ if (items.length > 1 || (item.kind === 'act' && item.asset)) return <ActGroup items={items} />;
173
+ if (item.kind === 'thought') return <ThoughtRow item={item} />;
174
+ return <ActRow item={item} />;
175
+ });
176
+
177
+ /** "3m 40s", "48s". Readable in every language and never widens a row. */
178
+ export function formatWorkDuration(ms: number): string {
179
+ const total = Math.max(1, Math.round(ms / 1000));
180
+ if (total < 60) return `${total}s`;
181
+ const minutes = Math.floor(total / 60);
182
+ if (minutes < 60) return `${minutes}m ${String(total % 60).padStart(2, '0')}s`;
183
+ return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`;
184
+ }
185
+
186
+ /**
187
+ * The agent thought for a while (only a local Codex / Claude Code reports this).
188
+ *
189
+ * While thinking, one shimmering row; then "Thought for 6s", which opens to the agent's own reasoning in
190
+ * muted gray. That text is written for itself, so it starts closed, as Codex and Claude Code show it.
191
+ */
192
+ function ThoughtRow({ item }: { item: AgentActivityItem }) {
193
+ const t = useT();
194
+ const [open, setOpen] = React.useState(false);
195
+ if (item.pending) {
196
+ return <div className="py-1 text-[14px] leading-relaxed"><span className="text-sweep">{t('agent.thinking')}</span></div>;
197
+ }
198
+ const ms = item.startedAt != null && item.endedAt != null ? item.endedAt - item.startedAt : null;
199
+ const label = ms != null && ms >= 1000
200
+ ? t('turnFrame.thought').replace('{t}', formatWorkDuration(ms))
201
+ : t('turnFrame.thoughtShort');
202
+ const text = (item.text ?? '').trim();
203
+ if (!text) return <div className="py-1 text-[14px] leading-relaxed text-[var(--chat-hint)]">{label}</div>;
204
+ return (
205
+ <div className="min-w-0 py-1">
206
+ <button
207
+ type="button"
208
+ aria-expanded={open}
209
+ onClick={() => setOpen((value) => !value)}
210
+ className="flex items-center gap-1.5 text-[14px] leading-relaxed text-[var(--chat-hint)] hover:text-[var(--chat-text)]"
211
+ >
212
+ {label}
213
+ <ChevronDown size={14} className={`transition-transform ${open ? 'rotate-180' : ''}`} />
214
+ </button>
215
+ {open ? (
216
+ <div className="mt-1 border-l border-[var(--border)] pl-3">
217
+ <Markdown text={text} muted />
218
+ </div>
219
+ ) : null}
220
+ </div>
221
+ );
222
+ }
223
+
224
+ /**
225
+ * The plan the agent sets itself (ACP's plan: Codex's update_plan, Claude Code's todos). Ticked off in
226
+ * place, not a new card per change. The current item is in text color, the rest gray, so progress shows
227
+ * at a glance.
228
+ */
229
+ function PlanRow({ item }: { item: AgentActivityItem }) {
230
+ const t = useT();
231
+ const plan = item.plan ?? [];
232
+ if (!plan.length) return null;
233
+ const done = plan.filter((entry) => entry.status === 'completed').length;
234
+ return (
235
+ <div className="min-w-0 py-1.5">
236
+ <div className="mb-1 text-[13px] text-[var(--chat-hint)]">
237
+ {t('turnFrame.plan').replace('{done}', String(done)).replace('{total}', String(plan.length))}
238
+ </div>
239
+ <ul className="flex flex-col gap-0.5">
240
+ {plan.map((entry, index) => {
241
+ const Icon = entry.status === 'completed' ? CircleCheck : entry.status === 'in_progress' ? CircleDot : Circle;
242
+ return (
243
+ <li key={`${index}-${entry.content}`} className="flex min-w-0 items-start gap-2 text-[14px] leading-relaxed">
244
+ <Icon
245
+ size={13}
246
+ aria-hidden
247
+ className={`mt-[5px] shrink-0 ${entry.status === 'in_progress' ? 'text-[var(--chat-text)]' : 'text-[var(--chat-hint)]'}`}
248
+ />
249
+ <span className={entry.status === 'in_progress' ? 'text-[var(--chat-text)]' : 'text-[var(--chat-hint)]'}>
250
+ {entry.content}
251
+ </span>
252
+ </li>
253
+ );
254
+ })}
255
+ </ul>
256
+ </div>
257
+ );
258
+ }
259
+
260
+ /**
261
+ * Asset URLs. The chat panel does not know the project; the project page provides a function that turns
262
+ * `assets/…` into a fetchable URL. Without one (the bench, other reuse) no preview is drawn.
263
+ */
264
+ export const ActAssetUrlContext = React.createContext<((src: string) => string) | null>(null);
265
+
266
+ /**
267
+ * One icon per act, so pictures, sound and checks are told apart at a glance; a column of equally
268
+ * gray text can only be read line by line.
269
+ */
270
+ const ACT_ICONS: Record<string, LucideIcon> = {
271
+ 'act.film.review': Eye,
272
+ 'act.film.frames': Eye,
273
+ 'act.film.hear': Ear,
274
+ 'act.film.check': CircleCheck,
275
+ 'act.film.arrange': Clapperboard,
276
+ 'act.scene.write': PenLine,
277
+ 'act.scene.edit': PenLine,
278
+ 'act.audio.transcribe': Captions,
279
+ 'act.audio.voice': UserRound,
280
+ 'act.audio.narrate': Mic,
281
+ 'act.audio.sfx': AudioLines,
282
+ 'act.audio.music': Music,
283
+ 'act.audio.translate': Languages,
284
+ 'act.asset.searchImage': Search,
285
+ 'act.asset.genImage': ImagePlus,
286
+ 'act.asset.genVideo': Video,
287
+ 'act.research.search': Globe,
288
+ 'act.research.read': FileText,
289
+ 'act.files.read': FileText,
290
+ 'act.files.search': FileSearch,
291
+ 'act.files.edit': FilePen,
292
+ 'act.command.run': SquareTerminal,
293
+ 'act.chat.ask': MessageCircleQuestion,
294
+ };
295
+
296
+ /**
297
+ * A folded group of finished acts.
298
+ *
299
+ * Closed by default. The summary says which kinds of act ran and how many of each (see actGroupParts);
300
+ * the pictures and sounds the group made stay in the rows of the acts that made them. The running row is
301
+ * not in here; it uses the progressive form and shimmers.
302
+ *
303
+ * Opening changes the content height. The asset drawer pins the bottom (pinIfFollowing in the same
304
+ * frame); here the clicked row must stay put instead: skip the next bottom pin and, in the same frame,
305
+ * put the row back where it was when clicked. Otherwise the observer drags the panel back to the bottom
306
+ * and the reply below scrolls away.
307
+ */
308
+ const GROUP_PARTS_MAX = 3;
309
+
310
+ function ActGroup({ items }: { items: AgentActivityItem[] }) {
311
+ const t = useT();
312
+ const thread = React.useContext(StickToBottomContext);
313
+ const rootRef = React.useRef<HTMLDivElement>(null);
314
+ const anchorTop = React.useRef<number | null>(null);
315
+ /* Closed until the user opens it — also while the turn runs. The summary counts up in place and the
316
+ status line under the turn says what is running, so unrolling each finished act only made a wall of rows. */
317
+ const [picked, setPicked] = React.useState<boolean | null>(null);
318
+ const open = picked ?? false;
319
+ const parts = React.useMemo(() => actGroupParts(items), [items]);
320
+ /* At most three kinds, the rest as a count: seven or eight cut off by an ellipsis tell the reader nothing. */
321
+ const shown = parts.slice(0, GROUP_PARTS_MAX);
322
+ const rest = parts.slice(GROUP_PARTS_MAX).reduce((n, part) => n + part.count, 0);
323
+ const summary = shown
324
+ .map((part) => `${t(actCopyKey(part.label, false))}${part.count > 1 ? ` ×${part.count}` : ''}`)
325
+ .join(' · ');
326
+ const full = parts
327
+ .map((part) => `${t(actCopyKey(part.label, false))}${part.count > 1 ? ` ×${part.count}` : ''}`)
328
+ .join(' · ');
329
+ const Icon = ACT_ICONS[parts[0]?.label ?? ''] ?? Layers;
330
+
331
+ const toggle = () => {
332
+ const node = rootRef.current;
333
+ if (node) {
334
+ thread?.skipNextPin();
335
+ anchorTop.current = node.getBoundingClientRect().top;
336
+ }
337
+ setPicked(!open);
338
+ };
339
+
340
+ React.useLayoutEffect(() => {
341
+ const node = rootRef.current;
342
+ const top = anchorTop.current;
343
+ const scroller = thread?.viewport ?? null;
344
+ anchorTop.current = null;
345
+ if (node == null || top == null || scroller == null) return;
346
+ const delta = node.getBoundingClientRect().top - top;
347
+ if (delta !== 0) scroller.scrollTop += delta;
348
+ }, [open, thread]);
349
+
350
+ return (
351
+ <div ref={rootRef} className="min-w-0 py-1">
352
+ {/* The chevron follows the text and shows on hover: a dozen groups with a column of chevrons read like a table */}
353
+ <button
354
+ type="button"
355
+ aria-expanded={open}
356
+ onClick={toggle}
357
+ title={full}
358
+ className="group/act flex max-w-full min-w-0 items-start gap-2 text-left text-[14px] leading-relaxed text-[var(--chat-hint)] hover:text-[var(--chat-text)]"
359
+ >
360
+ <Icon size={13} aria-hidden className="mt-[5px] shrink-0" />
361
+ <span className="min-w-0 truncate">{summary}</span>
362
+ {rest ? <span className="shrink-0 tabular-nums opacity-80">+{rest}</span> : null}
363
+ <ChevronRight
364
+ size={13}
365
+ aria-hidden
366
+ className={`mt-[5px] shrink-0 transition-[transform,opacity] ${open ? 'rotate-90 opacity-100' : 'opacity-0 group-hover/act:opacity-100 group-focus-visible/act:opacity-100'}`}
367
+ />
368
+ </button>
369
+ {open ? (
370
+ <div className="ml-[6px] mt-0.5 border-l border-[var(--border)] pl-3">
371
+ {items.map((item, index) => (item.kind === 'thought'
372
+ ? <ThoughtRow key={item.id ?? `thought-${index}`} item={item} />
373
+ : <ActRow key={item.id ?? `${item.label}-${index}`} item={item} />))}
374
+ </div>
375
+ ) : null}
376
+ </div>
377
+ );
378
+ }
379
+
380
+ /**
381
+ * One thing the agent is doing or has done.
382
+ *
383
+ * Finished rows just sit there: no tick, no red. A failed step means nothing actionable to someone who
384
+ * does not code; the agent usually fixes it next, and a red cross only suggests the film is broken. When
385
+ * the turn really fails, a generic message shows above the composer.
386
+ *
387
+ * A running row animates one thing only, the shimmer on its title: several animations per row, over ten
388
+ * rows, read like a dashboard alarm.
389
+ *
390
+ * The description (a narration line, a sound or picture description) is film content: it gets quotes and
391
+ * reads apart from the act name. What the act made hangs under the row: images as thumbnails, sounds play
392
+ * on click.
393
+ */
394
+ function ActRow({ item }: { item: AgentActivityItem }) {
395
+ const t = useT();
396
+ const Icon = ACT_ICONS[item.label];
397
+ return (
398
+ <div className="flex min-w-0 items-start gap-2 py-1 text-[14px] leading-relaxed">
399
+ {Icon ? (
400
+ <Icon size={13} aria-hidden className="mt-[5px] shrink-0 text-[var(--chat-hint)]" />
401
+ ) : null}
402
+ <div className="min-w-0 flex-1">
403
+ <div
404
+ className="flex max-w-full min-w-0 items-baseline gap-1.5 text-left"
405
+ >
406
+ <span className={item.pending ? 'text-sweep shrink-0' : 'shrink-0 text-[var(--chat-hint)]'}>
407
+ {t(actCopyKey(item.label, item.pending))}
408
+ </span>
409
+ {item.query ? (
410
+ <span className="min-w-0 truncate text-[var(--chat-hint)] opacity-80" title={item.query}>
411
+ “{item.query}”
412
+ </span>
413
+ ) : null}
414
+ {/* File name or command: not film content, so no quotes, monospace */}
415
+ {item.detail ? (
416
+ <span className="min-w-0 truncate font-mono text-[12.5px] text-[var(--chat-hint)] opacity-80" title={item.detail}>
417
+ {item.detail}
418
+ </span>
419
+ ) : null}
420
+ {item.pending ? <PendingElapsed startedAt={item.startedAt} /> : null}
421
+ </div>
422
+ {item.asset ? <AssetPreview asset={item.asset} /> : null}
423
+ {item.ask?.status ? <AskOutcome ask={item.ask} /> : null}
424
+ </div>
425
+ </div>
426
+ );
427
+ }
428
+
429
+ /** After a question: the answer (verbatim), or skipped / timed out, in which case the agent went on with its own judgment. */
430
+ function AskOutcome({ ask }: { ask: NonNullable<AgentActivityItem['ask']> }) {
431
+ const t = useT();
432
+ if (ask.status === 'answered' && ask.answer) {
433
+ return (
434
+ <div className="mt-1 flex min-w-0 items-start gap-1.5 text-[var(--chat-hint)]">
435
+ <CornerDownRight size={13} aria-hidden className="mt-[5px] shrink-0 text-[var(--chat-hint)]" />
436
+ <span className="min-w-0 whitespace-pre-wrap break-words">{ask.answer}</span>
437
+ </div>
438
+ );
439
+ }
440
+ return (
441
+ <div className="mt-1 flex items-center gap-1.5 text-[var(--chat-hint)]">
442
+ <CornerDownRight size={13} aria-hidden className="shrink-0" />
443
+ <span>{t(ask.status === 'timeout' ? 'turnAsk.timedOut' : 'turnAsk.skipped')}</span>
444
+ </div>
445
+ );
446
+ }
447
+
448
+ function AssetPreview({ asset }: { asset: AgentActAsset }) {
449
+ const toUrl = React.useContext(ActAssetUrlContext);
450
+ if (!toUrl) return null;
451
+ return (
452
+ <div className="mt-1.5">
453
+ {asset.kind === 'image' ? <ImageThumb asset={asset} />
454
+ /* The description is already on the row; the pill only needs play and duration. */
455
+ : asset.kind === 'audio' ? <AudioChip asset={asset} label="" />
456
+ : <MediaLink asset={asset} />}
457
+ </div>
458
+ );
459
+ }
460
+
461
+ /**
462
+ * A generated image. `loading="lazy"`: a long conversation has dozens, and off-screen ones should not load.
463
+ * A click opens the original enlarged on this page (not a new tab: this page may be running a generation).
464
+ * If it cannot load (a review frame evicted from cache, a deleted asset), nothing is drawn, not a broken image.
465
+ */
466
+ function ImageThumb({ asset }: { asset: AgentActAsset }) {
467
+ const t = useT();
468
+ const toUrl = React.useContext(ActAssetUrlContext)!;
469
+ const url = toUrl(asset.src);
470
+ const [gone, setGone] = React.useState(false);
471
+ const [open, setOpen] = React.useState(false);
472
+ if (gone) return null;
473
+ return (
474
+ <>
475
+ <button
476
+ type="button"
477
+ onClick={() => setOpen(true)}
478
+ title={t('actGroup.openImage')}
479
+ className="block shrink-0 overflow-hidden rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-opacity hover:opacity-85"
480
+ >
481
+ {/* eslint-disable-next-line @next/next/no-img-element */}
482
+ <img
483
+ src={url}
484
+ alt=""
485
+ loading="lazy"
486
+ decoding="async"
487
+ onError={() => setGone(true)}
488
+ className="max-h-48 max-w-full object-contain"
489
+ />
490
+ </button>
491
+ {open ? (
492
+ <AttachmentPreview
493
+ item={{ name: asset.src.split('/').pop() ?? asset.src, kind: 'image', url }}
494
+ onClose={() => setOpen(false)}
495
+ />
496
+ ) : null}
497
+ </>
498
+ );
499
+ }
500
+
501
+ function MediaLink({ asset }: { asset: AgentActAsset }) {
502
+ const toUrl = React.useContext(ActAssetUrlContext)!;
503
+ return (
504
+ <a
505
+ href={toUrl(asset.src)}
506
+ target="_blank"
507
+ rel="noopener noreferrer"
508
+ className="inline-flex h-7 items-center gap-1.5 rounded-full border border-[var(--border)] px-2.5 text-[12px] text-[var(--chat-hint)] hover:text-[var(--chat-text)]"
509
+ >
510
+ <Video size={12} aria-hidden />
511
+ {asset.dur ? formatDur(asset.dur) : null}
512
+ </a>
513
+ );
514
+ }
515
+
516
+ /**
517
+ * One sound. The panel shares one `<audio>`, so playing one stops the last: several sound effects at once
518
+ * are just noise, and a player per row means dozens of decoders and prefetches.
519
+ */
520
+ function AudioChip({ asset, label }: { asset: AgentActAsset; label?: string }) {
521
+ const t = useT();
522
+ const toUrl = React.useContext(ActAssetUrlContext)!;
523
+ const url = toUrl(asset.src);
524
+ const [missing, setMissing] = React.useState(false);
525
+ React.useEffect(() => {
526
+ const controller = new AbortController();
527
+ setMissing(false);
528
+ void fetch(url, { headers: { range: 'bytes=0-0' }, signal: controller.signal })
529
+ .then(async response => { setMissing(response.status === 404 || response.status === 410); await response.body?.cancel(); })
530
+ .catch(() => {});
531
+ return () => controller.abort();
532
+ }, [url]);
533
+ /* "Which one is playing" is tied to this button, not the URL: the agent can redo a file under the same
534
+ name (a redone sound effect is still pop-soft), so several buttons can point at one URL. */
535
+ const owner = React.useId();
536
+ const playing = React.useSyncExternalStore(subscribePlayer, () => playerOwner === owner, () => false);
537
+ const failed = React.useSyncExternalStore(subscribePlayer, () => playerFailedOwner === owner, () => false);
538
+ /* A sound is named by its description (the narration line, what the effect is); the file name is the
539
+ agent's internal name (s1, s2), used only when there is no description. */
540
+ const name = label ?? asset.src.split('/').pop()?.replace(/\.[a-z0-9]+$/i, '').replace(/[-_]+/g, ' ') ?? '';
541
+ if (missing) return null;
542
+ return (
543
+ <button
544
+ type="button"
545
+ onClick={() => togglePlayer(url, owner)}
546
+ aria-label={playing ? t('actGroup.pause') : t('actGroup.play')}
547
+ title={failed ? t('assets.previewFailed') : undefined}
548
+ className={`inline-flex h-7 max-w-full items-center gap-1.5 rounded-full border px-2.5 text-[12px] transition-colors ${
549
+ playing
550
+ ? 'border-[var(--accent)] text-[var(--chat-text)]'
551
+ : 'border-[var(--border)] text-[var(--chat-hint)] hover:text-[var(--chat-text)]'
552
+ }`}
553
+ >
554
+ {playing ? <Pause size={12} aria-hidden className="shrink-0" /> : <Play size={12} aria-hidden className="shrink-0" />}
555
+ {name ? <span className="min-w-0 max-w-[11rem] truncate">{name}</span> : null}
556
+ {asset.dur ? <span className="shrink-0 tabular-nums opacity-70">{formatDur(asset.dur)}</span> : null}
557
+ </button>
558
+ );
559
+ }
560
+
561
+ function formatDur(sec: number): string {
562
+ const s = Math.max(0, Math.round(sec));
563
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
564
+ }
565
+
566
+ /* ── The one player the panel shares ── */
567
+ let player: HTMLAudioElement | null = null;
568
+ /** The button now playing (its useId). */
569
+ let playerOwner: string | null = null;
570
+ let playerFailedOwner: string | null = null;
571
+ const playerListeners = new Set<() => void>();
572
+
573
+ function notifyPlayer(): void {
574
+ for (const listener of playerListeners) listener();
575
+ }
576
+
577
+ function subscribePlayer(listener: () => void): () => void {
578
+ playerListeners.add(listener);
579
+ return () => { playerListeners.delete(listener); };
580
+ }
581
+
582
+ function togglePlayer(url: string, owner: string): void {
583
+ if (typeof Audio === 'undefined') return;
584
+ if (!player) {
585
+ player = new Audio();
586
+ player.preload = 'none';
587
+ player.addEventListener('ended', () => { playerOwner = null; notifyPlayer(); });
588
+ player.addEventListener('error', () => { playerFailedOwner = playerOwner; playerOwner = null; notifyPlayer(); });
589
+ }
590
+ if (playerOwner === owner) {
591
+ player.pause();
592
+ playerOwner = null;
593
+ } else {
594
+ playerFailedOwner = null;
595
+ player.src = url;
596
+ playerOwner = owner;
597
+ void player.play().catch((e: unknown) => {
598
+ /* Switching sounds interrupts the previous play() with an AbortError: a switch, not a failure.
599
+ Reset only when this button itself cannot play. */
600
+ if ((e as { name?: string } | null)?.name === 'AbortError' || playerOwner !== owner) return;
601
+ playerOwner = null;
602
+ notifyPlayer();
603
+ });
604
+ }
605
+ notifyPlayer();
606
+ }
607
+
608
+ /**
609
+ * While a turn runs, the single row underneath: what is happening now.
610
+ *
611
+ * It stays from start to finish, always one row high, and only its text changes, which keeps the
612
+ * panel from jumping.
613
+ *
614
+ * · work running: that act (with its description or command when there is one); several in
615
+ * parallel read "Narrate ×7 · Sound effect ×6";
616
+ * · thinking: "Thinking";
617
+ * · neither: the caller's text (reconnecting and the like), else "Thinking".
618
+ *
619
+ * The clock counts from the earliest start among the running acts. When the turn ends this row becomes
620
+ * the footer, at the same height (h-6). `ownAgent`: the turn runs on the app's own agent (see IdleStatus).
621
+ */
622
+ export function StatusLine({ items, fallback, ownAgent = false }: { items: AgentActivityItem[]; fallback?: string | null; ownAgent?: boolean }) {
623
+ const t = useT();
624
+ const running = React.useMemo(() => pendingWork(items), [items]);
625
+ /* The last time anything happened: each new item (text, an act starting or finishing) gives the list a new identity */
626
+ // eslint-disable-next-line react-hooks/exhaustive-deps
627
+ const changedAt = React.useMemo(() => Date.now(), [items]);
628
+ /* something waits for the person's answer (a question, a permission, a spend, above the composer): what else runs
629
+ meanwhile is not what holds the turn up — no clock, no sweep */
630
+ const waitingForYou = React.useContext(WaitingContext);
631
+ if (waitingForYou) return <StatusShell Icon={MessageCircleQuestion} label={t('act.waitingForYou')} since={changedAt} waiting />;
632
+ const acts = running.filter((item) => item.kind === 'act');
633
+ if (acts.length) {
634
+ const parts = actGroupParts(acts);
635
+ const label = parts.slice(0, GROUP_PARTS_MAX)
636
+ .map((part) => `${t(part.label)}${part.count > 1 ? ` ×${part.count}` : ''}`)
637
+ .join(' · ');
638
+ const extra = acts.length === 1 ? (acts[0]!.query ? `“${acts[0]!.query}”` : acts[0]!.detail) : undefined;
639
+ let since: number | undefined;
640
+ for (const item of acts) if (item.startedAt != null && (since == null || item.startedAt < since)) since = item.startedAt;
641
+ /* Only waiting on the person's answer: that time is theirs, not the agent's work — no clock, no sweep. */
642
+ const waiting = acts.every((item) => item.label === 'act.chat.ask');
643
+ return <StatusShell Icon={ACT_ICONS[parts[0]!.label]} label={label} extra={extra} since={since ?? changedAt} waiting={waiting} />;
644
+ }
645
+ if (running.length || fallback) {
646
+ return <StatusShell label={running.length ? t('act.thinking') : fallback!} since={changedAt} />;
647
+ }
648
+ /* The agent is speaking (text still streaming): no "Working" underneath, the text itself moves. The row
649
+ keeps its height so what comes next does not jump. Just after a paragraph, stay blank a while (the
650
+ seconds spent wrapping up a turn, or an act about to start) and say "Working" only once it is quiet. */
651
+ let last: AgentActivityItem | undefined;
652
+ for (let i = items.length - 1; i >= 0; i -= 1) if (items[i]!.kind !== 'notice') { last = items[i]; break; }
653
+ if (last?.kind === 'text' && last.pending) return <StatusBlank />;
654
+ return <IdleStatus since={changedAt} quietFirst={last?.kind === 'text'} ownAgent={ownAgent} />;
655
+ }
656
+
657
+ /** The status row's empty slot: same height, says nothing. */
658
+ function StatusBlank() {
659
+ return <div role="status" aria-hidden className="h-6" />;
660
+ }
661
+
662
+ /** How long after a paragraph before saying "Working". */
663
+ const AFTER_TEXT_MS = 5_000;
664
+
665
+ /**
666
+ * The stretch where nothing is reported.
667
+ *
668
+ * When a local Codex / Claude Code writes a whole scene file, the command is reported only once it is
669
+ * written, which can be two minutes of silence. With only a stop button showing, that looks frozen. So
670
+ * the row stays: "Working" plus the seconds since the last activity, "Still working" after half a minute
671
+ * of quiet. A moving clock shows it is alive.
672
+ *
673
+ * The app's own agent says "Waiting for the model" after two minutes, with why as the row's faint words: some
674
+ * models think for minutes before the first word. Those words go in the row, not on a line of their own, so it
675
+ * stays one row high.
676
+ */
677
+ function IdleStatus({ since, quietFirst = false, ownAgent = false }: { since: number; quietFirst?: boolean; ownAgent?: boolean }) {
678
+ const t = useT();
679
+ const tick = useTick();
680
+ const idle = (tick ?? Date.now()) - since;
681
+ if (quietFirst && idle < AFTER_TEXT_MS) return <StatusBlank />;
682
+ const label = idleStatus(idle, ownAgent);
683
+ return <StatusShell label={t(label)} extra={label === 'act.waitingForModel' ? t('act.waitingForModelHint') : undefined} plain since={since} />;
684
+ }
685
+
686
+ /* Status row: the act's icon while an act runs; thinking / working is text only, flush left. The shimmer
687
+ already says "in progress", so no spinner and no empty slot pushing the text right. */
688
+ function StatusShell({ Icon, label, extra, plain = false, since, waiting = false }: { Icon?: LucideIcon | undefined; label: string; extra?: string | undefined; plain?: boolean; since: number; waiting?: boolean }) {
689
+ return (
690
+ <div role="status" className="flex h-6 min-w-0 items-center gap-2 text-[14px]">
691
+ {Icon ? <Icon size={13} aria-hidden className="shrink-0 text-[var(--chat-hint)]" /> : null}
692
+ <span className={`${waiting ? '' : 'text-sweep '}min-w-0 truncate ${extra ? 'max-w-[60%] shrink-0' : ''}`}>{label}</span>
693
+ {extra ? (
694
+ <span className={`min-w-0 truncate text-[var(--chat-hint)] opacity-80 ${plain || extra.startsWith('“') ? '' : 'font-mono text-[12.5px]'}`} title={extra}>
695
+ {extra}
696
+ </span>
697
+ ) : null}
698
+ {waiting ? null : <PendingElapsed startedAt={since} />}
699
+ </div>
700
+ );
701
+ }
702
+
703
+ /**
704
+ * The running row's clock. The once-a-second subscription lives only here, so finished rows (hundreds
705
+ * in a long conversation) do not re-render every second.
706
+ */
707
+ function PendingElapsed({ startedAt }: { startedAt: AgentActivityItem['startedAt'] }) {
708
+ const tick = useTick();
709
+ const elapsed = elapsedLabel(startedAt, tick);
710
+ if (!elapsed) return null;
711
+ return (
712
+ <span className="ml-auto shrink-0 tabular-nums text-[11px] text-[var(--chat-hint)]">
713
+ {elapsed}
714
+ </span>
715
+ );
716
+ }