@agent-native/core 0.79.27 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +41 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +51 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +50 -0
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +49 -0
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +44 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +46 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +44 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +48 -0
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +36 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +38 -0
  13. package/corpus/core/docs/content/template-design.mdx +45 -0
  14. package/corpus/core/package.json +2 -1
  15. package/corpus/core/src/agent/engine/index.ts +1 -0
  16. package/corpus/core/src/agent/engine/registry.ts +40 -0
  17. package/corpus/core/src/agent/production-agent.ts +6 -2
  18. package/corpus/core/src/cli/design-connect.ts +490 -0
  19. package/corpus/core/src/cli/index.ts +16 -0
  20. package/corpus/core/src/cli/skills.ts +126 -3
  21. package/corpus/core/src/client/AssistantChat.tsx +43 -7
  22. package/corpus/core/src/client/agent-chat.ts +27 -3
  23. package/corpus/core/src/client/blocks/library/MermaidBlock.tsx +1 -1
  24. package/corpus/core/src/client/blocks/library/diagram.tsx +2 -2
  25. package/corpus/core/src/client/blocks/library/wireframe.tsx +1 -1
  26. package/corpus/core/src/client/chat/message-components.tsx +1 -1
  27. package/corpus/core/src/client/chat-model-groups.ts +1 -2
  28. package/corpus/core/src/client/components/LiveCursorOverlay.tsx +33 -66
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +99 -24
  30. package/corpus/core/src/client/composer/useVoiceDictation.ts +429 -260
  31. package/corpus/core/src/client/frame-protocol.ts +17 -2
  32. package/corpus/core/src/client/rich-markdown-editor/RegistryBlockNode.tsx +2 -2
  33. package/corpus/core/src/client/sharing/ShareButton.tsx +20 -15
  34. package/corpus/core/src/client/use-agent-engine-configured.ts +80 -28
  35. package/corpus/core/src/server/transcribe-voice.ts +57 -22
  36. package/corpus/core/src/shared/mcp-embed-headers.ts +23 -1
  37. package/corpus/core/src/styles/agent-native.css +56 -7
  38. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  39. package/corpus/core/src/vite/client.ts +2 -0
  40. package/corpus/core/src/voice/index.ts +18 -0
  41. package/corpus/core/src/voice/voice-cleanup-prompt.ts +38 -0
  42. package/corpus/core/src/voice/voice-context.ts +217 -0
  43. package/corpus/core/src/voice/voice-replacements.ts +75 -0
  44. package/corpus/templates/analytics/AGENTS.md +4 -3
  45. package/corpus/templates/analytics/actions/list-session-recordings.ts +6 -1
  46. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +557 -400
  47. package/corpus/templates/analytics/app/global.css +0 -4
  48. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  49. package/corpus/templates/analytics/app/i18n-data.ts +20 -0
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +33 -8
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +7 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +7 -0
  53. package/corpus/templates/analytics/changelog/2026-06-28-ask-tab-now-matches-the-darker-app-background.md +6 -0
  54. package/corpus/templates/analytics/changelog/2026-06-28-dashboard-email-reports-now-use-cleaner-light-mode-snapshots.md +6 -0
  55. package/corpus/templates/analytics/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  56. package/corpus/templates/analytics/changelog/2026-06-28-sessions-now-show-only-signed-in-recordings-with-playable-re.md +6 -0
  57. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +3 -3
  58. package/corpus/templates/analytics/server/lib/dashboard-report.ts +29 -7
  59. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +5 -2
  60. package/corpus/templates/analytics/server/lib/session-replay.ts +24 -4
  61. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -4
  62. package/corpus/templates/assets/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  63. package/corpus/templates/brain/app/components/layout/Layout.tsx +29 -2
  64. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +131 -42
  65. package/corpus/templates/brain/app/i18n/zh-TW.ts +2 -0
  66. package/corpus/templates/brain/app/i18n-data.ts +20 -0
  67. package/corpus/templates/brain/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  68. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +29 -1
  69. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +417 -299
  70. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  71. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  72. package/corpus/templates/calendar/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  73. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +6 -19
  74. package/corpus/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  75. package/corpus/templates/clips/AGENTS.md +4 -0
  76. package/corpus/templates/clips/actions/cleanup-transcript.ts +39 -9
  77. package/corpus/templates/clips/actions/finalize-meeting.ts +81 -12
  78. package/corpus/templates/clips/actions/lib/agents-md-context.ts +4 -1
  79. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -3
  80. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +1 -1
  81. package/corpus/templates/clips/app/components/player/share-dialog.tsx +2 -2
  82. package/corpus/templates/clips/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  83. package/corpus/templates/clips/desktop/src/lib/personal-vocabulary.ts +13 -8
  84. package/corpus/templates/clips/desktop/src/lib/voice-dictation.ts +60 -1
  85. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1 -1
  86. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -1
  87. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -5
  88. package/corpus/templates/content/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  89. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +137 -0
  90. package/corpus/templates/design/AGENTS.md +95 -5
  91. package/corpus/templates/design/DESIGN.md +82 -0
  92. package/corpus/templates/design/DEVELOPING.md +43 -0
  93. package/corpus/templates/design/actions/add-localhost-screens.ts +426 -0
  94. package/corpus/templates/design/actions/apply-shader.ts +277 -0
  95. package/corpus/templates/design/actions/apply-visual-edit.ts +303 -0
  96. package/corpus/templates/design/actions/connect-localhost.ts +183 -0
  97. package/corpus/templates/design/actions/edit-design.ts +195 -42
  98. package/corpus/templates/design/actions/generate-design.ts +71 -1
  99. package/corpus/templates/design/actions/generate-screens.ts +193 -0
  100. package/corpus/templates/design/actions/get-code-layer-projection.ts +191 -0
  101. package/corpus/templates/design/actions/get-shader.ts +119 -0
  102. package/corpus/templates/design/actions/list-localhost-connections.ts +95 -0
  103. package/corpus/templates/design/actions/navigate.ts +55 -2
  104. package/corpus/templates/design/actions/view-screen.ts +68 -1
  105. package/corpus/templates/design/agent-native.app-skill.json +10 -0
  106. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +761 -0
  107. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +2057 -103
  108. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +74 -38
  109. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +399 -0
  110. package/corpus/templates/design/app/components/design/EditPanel.tsx +3185 -439
  111. package/corpus/templates/design/app/components/design/LayersPanel.tsx +1124 -0
  112. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +3116 -147
  113. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +700 -13
  114. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +107 -63
  115. package/corpus/templates/design/app/components/design/index.ts +0 -5
  116. package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +286 -0
  117. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +699 -0
  118. package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +461 -0
  119. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +166 -0
  120. package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +1420 -0
  121. package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +420 -0
  122. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +215 -0
  123. package/corpus/templates/design/app/components/design/inspector/ShaderControls.stories.tsx +71 -0
  124. package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +518 -0
  125. package/corpus/templates/design/app/components/design/inspector/ShaderEffectRow.tsx +106 -0
  126. package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +361 -0
  127. package/corpus/templates/design/app/components/design/inspector/index.ts +60 -0
  128. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +201 -0
  129. package/corpus/templates/design/app/components/design/types.ts +23 -0
  130. package/corpus/templates/design/app/components/layout/Layout.tsx +24 -13
  131. package/corpus/templates/design/app/components/layout/Sidebar.tsx +4 -4
  132. package/corpus/templates/design/app/components/visual-editor/CanvasCommentPins.tsx +474 -36
  133. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +22 -5
  134. package/corpus/templates/design/app/components/visual-editor/SaveStatusIndicator.tsx +24 -11
  135. package/corpus/templates/design/app/global.css +121 -3
  136. package/corpus/templates/design/app/hooks/use-agent-generating.ts +4 -0
  137. package/corpus/templates/design/app/hooks/use-navigation-state.ts +130 -8
  138. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +329 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +264 -0
  140. package/corpus/templates/design/app/i18n-data.ts +2790 -1
  141. package/corpus/templates/design/app/pages/DesignEditor.tsx +5108 -851
  142. package/corpus/templates/design/changelog/2026-06-28-annotate-mode-now-combines-drawing-and-comment-pins-in-one-c.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-28-design-editor-panel-borders-are-cleaner-so-the-canvas-edge-s.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-28-drawing-prompts-now-reliably-open-and-submit-to-the-design-a.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-28-editor-app-menus-now-use-an-outline-agent-native-mark-with-b.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-28-figma-style-code-design-mode.md +6 -0
  147. package/corpus/templates/design/changelog/2026-06-28-layer-selection-now-updates-chat-context-without-opening-the.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-28-layer-selections-now-preserve-hidden-and-locked-canvas-state.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-28-live-cursors-now-keep-names-beside-a-cleaner-pointer-marker.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-28-mode-switching-now-lives-in-a-compact-bottom-right-icon-dock.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-handles-zooming-drawing-opening-and-dele.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-28-screen-overview-now-selects-screens-on-click-and-opens-them-.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-28-screen-overview-zooms-into-the-active-screen-with-smoother-e.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-28-selected-element-chips-in-chat-are-shorter-and-easier-to-sca.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-28-the-design-editor-toolbar-is-more-compact-and-the-app-menu-m.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-at-the-top-of-the-design-sidebar-instead-of-.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-28-tweaks-now-live-in-the-right-inspector-beside-the-design-tab.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-active-sidebar-rows-use-soft-highlights.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-agents-can-now-open-screen-overview-focus-screens-and-add.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-edit-broad-copy-changes-more-reliably.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-design-editor-extensions-now-run-inline-beside-the-artboard.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-sidebars-use-darker-chrome-and-chat-warns-immediately.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-visual-edit-can-open-localhost-routes-as-url-backed-screens.md +6 -0
  165. package/corpus/templates/design/package.json +1 -0
  166. package/corpus/templates/design/server/db/schema.ts +26 -0
  167. package/corpus/templates/design/server/lib/design-snapshot.ts +1 -1
  168. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -0
  169. package/corpus/templates/design/shared/api.ts +27 -0
  170. package/corpus/templates/design/shared/canvas-frames.ts +103 -0
  171. package/corpus/templates/design/shared/canvas-math.ts +1058 -0
  172. package/corpus/templates/design/shared/code-layer.ts +2212 -0
  173. package/corpus/templates/design/shared/color-utils.ts +180 -0
  174. package/corpus/templates/design/shared/generation-session.ts +61 -0
  175. package/corpus/templates/design/shared/shader-presets.ts +782 -0
  176. package/corpus/templates/design/shared/shader-safety.ts +137 -0
  177. package/corpus/templates/design/shared/source-mode.ts +237 -0
  178. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +149 -6
  179. package/corpus/templates/forms/app/i18n/ar-SA.ts +2 -0
  180. package/corpus/templates/forms/app/i18n/de-DE.ts +2 -0
  181. package/corpus/templates/forms/app/i18n/en-US.ts +2 -0
  182. package/corpus/templates/forms/app/i18n/es-ES.ts +2 -0
  183. package/corpus/templates/forms/app/i18n/fr-FR.ts +2 -0
  184. package/corpus/templates/forms/app/i18n/hi-IN.ts +2 -0
  185. package/corpus/templates/forms/app/i18n/ja-JP.ts +2 -0
  186. package/corpus/templates/forms/app/i18n/ko-KR.ts +2 -0
  187. package/corpus/templates/forms/app/i18n/pt-BR.ts +2 -0
  188. package/corpus/templates/forms/app/i18n/zh-CN.ts +2 -0
  189. package/corpus/templates/forms/app/i18n/zh-TW.ts +2 -0
  190. package/corpus/templates/forms/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  191. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +2 -2
  192. package/corpus/templates/macros/changelog/2026-06-28-left-sidebar-footer-controls-use-less-divider-chrome.md +6 -0
  193. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +408 -247
  194. package/corpus/templates/mail/app/i18n/ar-SA.ts +4 -0
  195. package/corpus/templates/mail/app/i18n/de-DE.ts +4 -0
  196. package/corpus/templates/mail/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/mail/app/i18n/es-ES.ts +4 -0
  198. package/corpus/templates/mail/app/i18n/fr-FR.ts +4 -0
  199. package/corpus/templates/mail/app/i18n/hi-IN.ts +4 -0
  200. package/corpus/templates/mail/app/i18n/ja-JP.ts +4 -0
  201. package/corpus/templates/mail/app/i18n/ko-KR.ts +4 -0
  202. package/corpus/templates/mail/app/i18n/pt-BR.ts +4 -0
  203. package/corpus/templates/mail/app/i18n/zh-CN.ts +4 -0
  204. package/corpus/templates/mail/app/i18n/zh-TW.ts +4 -0
  205. package/corpus/templates/mail/changelog/2026-06-28-pinned-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  206. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +6 -8
  207. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +1 -1
  208. package/corpus/templates/plan/app/global.css +61 -78
  209. package/corpus/templates/plan/app/pages/PlansPage.tsx +8 -0
  210. package/corpus/templates/plan/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  211. package/corpus/templates/plan/changelog/2026-06-28-plans-use-the-shared-neutral-light-and-dark-theme.md +6 -0
  212. package/corpus/templates/plan/changelog/2026-06-28-sending-open-feedback-from-a-plan-now-opens-the-inline-plan-.md +6 -0
  213. package/corpus/templates/slides/app/components/layout/Sidebar.tsx +5 -5
  214. package/corpus/templates/slides/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +6 -0
  215. package/corpus/templates/videos/app/components/Sidebar.tsx +2 -2
  216. package/corpus/templates/videos/app/components/layout/Layout.tsx +29 -2
  217. package/corpus/templates/videos/app/components/layout/NavSidebar.tsx +113 -30
  218. package/corpus/templates/videos/app/i18n/ar-SA.ts +2 -0
  219. package/corpus/templates/videos/app/i18n/de-DE.ts +2 -0
  220. package/corpus/templates/videos/app/i18n/en-US.ts +2 -0
  221. package/corpus/templates/videos/app/i18n/es-ES.ts +2 -0
  222. package/corpus/templates/videos/app/i18n/fr-FR.ts +2 -0
  223. package/corpus/templates/videos/app/i18n/hi-IN.ts +2 -0
  224. package/corpus/templates/videos/app/i18n/ja-JP.ts +2 -0
  225. package/corpus/templates/videos/app/i18n/ko-KR.ts +2 -0
  226. package/corpus/templates/videos/app/i18n/pt-BR.ts +2 -0
  227. package/corpus/templates/videos/app/i18n/zh-CN.ts +2 -0
  228. package/corpus/templates/videos/app/i18n/zh-TW.ts +2 -0
  229. package/corpus/templates/videos/changelog/2026-06-28-left-sidebar-collapses-into-an-animated-icon-rail.md +6 -0
  230. package/dist/agent/engine/index.d.ts +1 -1
  231. package/dist/agent/engine/index.d.ts.map +1 -1
  232. package/dist/agent/engine/index.js +1 -1
  233. package/dist/agent/engine/index.js.map +1 -1
  234. package/dist/agent/engine/registry.d.ts +9 -0
  235. package/dist/agent/engine/registry.d.ts.map +1 -1
  236. package/dist/agent/engine/registry.js +37 -0
  237. package/dist/agent/engine/registry.js.map +1 -1
  238. package/dist/agent/production-agent.d.ts.map +1 -1
  239. package/dist/agent/production-agent.js +4 -3
  240. package/dist/agent/production-agent.js.map +1 -1
  241. package/dist/cli/design-connect.d.ts +52 -0
  242. package/dist/cli/design-connect.d.ts.map +1 -0
  243. package/dist/cli/design-connect.js +409 -0
  244. package/dist/cli/design-connect.js.map +1 -0
  245. package/dist/cli/index.js +15 -0
  246. package/dist/cli/index.js.map +1 -1
  247. package/dist/cli/skills.d.ts +3 -0
  248. package/dist/cli/skills.d.ts.map +1 -1
  249. package/dist/cli/skills.js +121 -3
  250. package/dist/cli/skills.js.map +1 -1
  251. package/dist/client/AssistantChat.d.ts.map +1 -1
  252. package/dist/client/AssistantChat.js +38 -8
  253. package/dist/client/AssistantChat.js.map +1 -1
  254. package/dist/client/agent-chat.d.ts +6 -0
  255. package/dist/client/agent-chat.d.ts.map +1 -1
  256. package/dist/client/agent-chat.js +17 -3
  257. package/dist/client/agent-chat.js.map +1 -1
  258. package/dist/client/blocks/library/MermaidBlock.js +1 -1
  259. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  260. package/dist/client/blocks/library/diagram.js +1 -1
  261. package/dist/client/blocks/library/diagram.js.map +1 -1
  262. package/dist/client/blocks/library/wireframe.js +1 -1
  263. package/dist/client/blocks/library/wireframe.js.map +1 -1
  264. package/dist/client/chat/message-components.js +1 -1
  265. package/dist/client/chat/message-components.js.map +1 -1
  266. package/dist/client/chat-model-groups.d.ts.map +1 -1
  267. package/dist/client/chat-model-groups.js +1 -2
  268. package/dist/client/chat-model-groups.js.map +1 -1
  269. package/dist/client/components/LiveCursorOverlay.d.ts +1 -2
  270. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -1
  271. package/dist/client/components/LiveCursorOverlay.js +19 -40
  272. package/dist/client/components/LiveCursorOverlay.js.map +1 -1
  273. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  274. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  275. package/dist/client/composer/TiptapComposer.js +86 -24
  276. package/dist/client/composer/TiptapComposer.js.map +1 -1
  277. package/dist/client/composer/useVoiceDictation.d.ts +9 -0
  278. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -1
  279. package/dist/client/composer/useVoiceDictation.js +154 -32
  280. package/dist/client/composer/useVoiceDictation.js.map +1 -1
  281. package/dist/client/frame-protocol.d.ts +17 -2
  282. package/dist/client/frame-protocol.d.ts.map +1 -1
  283. package/dist/client/frame-protocol.js.map +1 -1
  284. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +2 -2
  285. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  286. package/dist/client/sharing/ShareButton.d.ts +2 -0
  287. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  288. package/dist/client/sharing/ShareButton.js +7 -6
  289. package/dist/client/sharing/ShareButton.js.map +1 -1
  290. package/dist/client/use-agent-engine-configured.d.ts +5 -0
  291. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  292. package/dist/client/use-agent-engine-configured.js +51 -25
  293. package/dist/client/use-agent-engine-configured.js.map +1 -1
  294. package/dist/collab/awareness.d.ts +2 -2
  295. package/dist/collab/awareness.d.ts.map +1 -1
  296. package/dist/collab/routes.d.ts +1 -1
  297. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  298. package/dist/notifications/routes.d.ts +3 -3
  299. package/dist/observability/routes.d.ts +8 -8
  300. package/dist/progress/routes.d.ts +1 -1
  301. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  302. package/dist/server/transcribe-voice.d.ts.map +1 -1
  303. package/dist/server/transcribe-voice.js +40 -24
  304. package/dist/server/transcribe-voice.js.map +1 -1
  305. package/dist/shared/mcp-embed-headers.d.ts +1 -0
  306. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  307. package/dist/shared/mcp-embed-headers.js +20 -1
  308. package/dist/shared/mcp-embed-headers.js.map +1 -1
  309. package/dist/styles/agent-native.css +56 -7
  310. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  311. package/dist/vite/client.d.ts.map +1 -1
  312. package/dist/vite/client.js +2 -0
  313. package/dist/vite/client.js.map +1 -1
  314. package/dist/voice/index.d.ts +5 -0
  315. package/dist/voice/index.d.ts.map +1 -0
  316. package/dist/voice/index.js +4 -0
  317. package/dist/voice/index.js.map +1 -0
  318. package/dist/voice/voice-cleanup-prompt.d.ts +6 -0
  319. package/dist/voice/voice-cleanup-prompt.d.ts.map +1 -0
  320. package/dist/voice/voice-cleanup-prompt.js +26 -0
  321. package/dist/voice/voice-cleanup-prompt.js.map +1 -0
  322. package/dist/voice/voice-context.d.ts +25 -0
  323. package/dist/voice/voice-context.d.ts.map +1 -0
  324. package/dist/voice/voice-context.js +156 -0
  325. package/dist/voice/voice-context.js.map +1 -0
  326. package/dist/voice/voice-replacements.d.ts +4 -0
  327. package/dist/voice/voice-replacements.d.ts.map +1 -0
  328. package/dist/voice/voice-replacements.js +52 -0
  329. package/dist/voice/voice-replacements.js.map +1 -0
  330. package/docs/content/locales/ar-SA/template-design.mdx +41 -0
  331. package/docs/content/locales/de-DE/template-design.mdx +51 -0
  332. package/docs/content/locales/es-ES/template-design.mdx +50 -0
  333. package/docs/content/locales/fr-FR/template-design.mdx +49 -0
  334. package/docs/content/locales/hi-IN/template-design.mdx +44 -0
  335. package/docs/content/locales/ja-JP/template-design.mdx +46 -0
  336. package/docs/content/locales/ko-KR/template-design.mdx +44 -0
  337. package/docs/content/locales/pt-BR/template-design.mdx +48 -0
  338. package/docs/content/locales/zh-CN/template-design.mdx +36 -0
  339. package/docs/content/locales/zh-TW/template-design.mdx +38 -0
  340. package/docs/content/template-design.mdx +45 -0
  341. package/package.json +2 -1
  342. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +10 -0
  343. package/corpus/templates/design/app/components/design/DesignToolbar.tsx +0 -339
  344. package/corpus/templates/design/app/components/design/PresentMode.tsx +0 -68
  345. package/corpus/templates/design/app/components/design/ViewportTabs.tsx +0 -149
  346. package/corpus/templates/design/app/components/design/ZoomControls.tsx +0 -157
@@ -20,6 +20,10 @@
20
20
 
21
21
  import { useCallback, useEffect, useRef, useState } from "react";
22
22
 
23
+ import {
24
+ applyVoiceContextReplacements,
25
+ type VoiceContextPack,
26
+ } from "../../voice/index.js";
23
27
  import { agentNativePath } from "../api-path.js";
24
28
 
25
29
  export type VoiceProvider =
@@ -38,12 +42,26 @@ export interface VoicePrefs {
38
42
  provider: VoiceProvider;
39
43
  transcriptionMode?: TranscriptionMode;
40
44
  instructions?: string;
45
+ cleanupEnabled?: boolean;
41
46
  }
42
47
 
48
+ export interface VoiceCleanupPrefs {
49
+ enabled?: boolean;
50
+ }
51
+
52
+ type VoiceContextPackSource =
53
+ | VoiceContextPack
54
+ | (() => VoiceContextPack | undefined | Promise<VoiceContextPack | undefined>)
55
+ | undefined;
56
+
43
57
  const PREFS_KEY = "voice-transcription-prefs";
58
+ const CLEANUP_PREFS_KEY = "voice-cleanup-prefs";
44
59
  const PREFS_URL = agentNativePath(
45
60
  `/_agent-native/application-state/${PREFS_KEY}`,
46
61
  );
62
+ const CLEANUP_PREFS_URL = agentNativePath(
63
+ `/_agent-native/application-state/${CLEANUP_PREFS_KEY}`,
64
+ );
47
65
  const TRANSCRIBE_URL = agentNativePath("/_agent-native/transcribe-voice");
48
66
  const GOOGLE_REALTIME_SESSION_URL = agentNativePath(
49
67
  "/_agent-native/transcribe-stream/session",
@@ -98,6 +116,7 @@ export interface UseVoiceDictationOptions {
98
116
  onError?: (message: string) => void;
99
117
  /** Called with (accumulatedFinalText, currentInterimText) as speech is recognized in real time. */
100
118
  onLiveUpdate?: (finalText: string, interimText: string) => void;
119
+ contextPack?: VoiceContextPackSource;
101
120
  }
102
121
 
103
122
  export interface VoiceDictationApi {
@@ -114,9 +133,15 @@ export interface VoiceDictationApi {
114
133
  }
115
134
 
116
135
  async function readVoicePrefs(): Promise<VoicePrefs> {
136
+ const cleanupPrefs = await readVoiceCleanupPrefs();
117
137
  try {
118
138
  const res = await fetch(PREFS_URL);
119
- if (!res.ok) return { provider: await defaultProvider() };
139
+ if (!res.ok) {
140
+ return {
141
+ provider: await defaultProvider(),
142
+ cleanupEnabled: cleanupPrefs.enabled,
143
+ };
144
+ }
120
145
  const body = (await res.json()) as
121
146
  | VoicePrefs
122
147
  | { value?: VoicePrefs }
@@ -141,6 +166,7 @@ async function readVoicePrefs(): Promise<VoicePrefs> {
141
166
  return {
142
167
  transcriptionMode: mode,
143
168
  provider,
169
+ cleanupEnabled: cleanupPrefs.enabled,
144
170
  instructions:
145
171
  typeof instructions === "string" ? instructions.trim() : undefined,
146
172
  };
@@ -148,7 +174,27 @@ async function readVoicePrefs(): Promise<VoicePrefs> {
148
174
  } catch {
149
175
  /* fall through */
150
176
  }
151
- return { provider: await defaultProvider() };
177
+ return {
178
+ provider: await defaultProvider(),
179
+ cleanupEnabled: cleanupPrefs.enabled,
180
+ };
181
+ }
182
+
183
+ async function readVoiceCleanupPrefs(): Promise<VoiceCleanupPrefs> {
184
+ try {
185
+ const res = await fetch(CLEANUP_PREFS_URL);
186
+ if (!res.ok) return {};
187
+ const body = (await res.json()) as
188
+ | VoiceCleanupPrefs
189
+ | { value?: VoiceCleanupPrefs }
190
+ | null;
191
+ const value =
192
+ (body as { value?: VoiceCleanupPrefs } | null)?.value ??
193
+ (body as VoiceCleanupPrefs | null);
194
+ return { enabled: value?.enabled === true };
195
+ } catch {
196
+ return {};
197
+ }
152
198
  }
153
199
 
154
200
  function getSpeechRecognitionCtor(): any {
@@ -178,6 +224,70 @@ function pickMimeType(): string {
178
224
  return "audio/webm";
179
225
  }
180
226
 
227
+ async function resolveVoiceContextPack(
228
+ source: VoiceContextPackSource,
229
+ ): Promise<VoiceContextPack | undefined> {
230
+ try {
231
+ const value = typeof source === "function" ? await source() : source;
232
+ return value && typeof value === "object" ? value : undefined;
233
+ } catch {
234
+ return undefined;
235
+ }
236
+ }
237
+
238
+ function appendVoiceContext(
239
+ form: FormData,
240
+ contextPack: VoiceContextPack | undefined,
241
+ ): void {
242
+ if (!contextPack) return;
243
+ try {
244
+ form.append("voiceContext", JSON.stringify(contextPack));
245
+ } catch {
246
+ /* best effort */
247
+ }
248
+ }
249
+
250
+ export function providerForTextCleanup(provider: VoiceProvider): VoiceProvider {
251
+ return provider === "google-realtime" ? "auto" : provider;
252
+ }
253
+
254
+ async function cleanupRecognizedText({
255
+ text,
256
+ provider,
257
+ instructions,
258
+ contextPack,
259
+ }: {
260
+ text: string;
261
+ provider: VoiceProvider;
262
+ instructions?: string;
263
+ contextPack?: VoiceContextPack;
264
+ }): Promise<string> {
265
+ if (provider === "browser") {
266
+ return applyVoiceContextReplacements(text, contextPack);
267
+ }
268
+
269
+ const form = new FormData();
270
+ form.append("text", text);
271
+ form.append("provider", providerForTextCleanup(provider));
272
+ if (instructions?.trim()) {
273
+ form.append("instructions", instructions.trim());
274
+ }
275
+ appendVoiceContext(form, contextPack);
276
+
277
+ const res = await fetch(TRANSCRIBE_URL, {
278
+ method: "POST",
279
+ body: form,
280
+ });
281
+ if (!res.ok) {
282
+ const body = await res
283
+ .json()
284
+ .catch(() => ({ error: `HTTP ${res.status}` }));
285
+ throw new Error(body.error || `Transcript cleanup failed (${res.status})`);
286
+ }
287
+ const data = (await res.json()) as { text?: string };
288
+ return (data.text ?? "").trim() || text;
289
+ }
290
+
181
291
  type BrowserDocumentPolicy = {
182
292
  allowsFeature: (feature: string) => boolean;
183
293
  };
@@ -249,13 +359,15 @@ function voiceDictationSpeechErrorMessage(error: string | undefined): string {
249
359
  export function useVoiceDictation(
250
360
  options: UseVoiceDictationOptions,
251
361
  ): VoiceDictationApi {
252
- const { onTranscript, onError, onLiveUpdate } = options;
362
+ const { onTranscript, onError, onLiveUpdate, contextPack } = options;
253
363
  const onTranscriptRef = useRef(onTranscript);
254
364
  const onErrorRef = useRef(onError);
255
365
  const onLiveUpdateRef = useRef(onLiveUpdate);
366
+ const contextPackRef = useRef(contextPack);
256
367
  onTranscriptRef.current = onTranscript;
257
368
  onErrorRef.current = onError;
258
369
  onLiveUpdateRef.current = onLiveUpdate;
370
+ contextPackRef.current = contextPack;
259
371
 
260
372
  const [state, setState] = useState<VoiceState>("idle");
261
373
  const [amplitude, setAmplitude] = useState(0);
@@ -366,6 +478,11 @@ export function useVoiceDictation(
366
478
  [teardown],
367
479
  );
368
480
 
481
+ const getVoiceContextPack = useCallback(
482
+ () => resolveVoiceContextPack(contextPackRef.current),
483
+ [],
484
+ );
485
+
369
486
  const startMeter = useCallback((stream: MediaStream) => {
370
487
  try {
371
488
  const AudioCtor =
@@ -456,6 +573,7 @@ export function useVoiceDictation(
456
573
  if (instructions?.trim()) {
457
574
  form.append("instructions", instructions.trim());
458
575
  }
576
+ appendVoiceContext(form, await getVoiceContextPack());
459
577
  const res = await fetch(TRANSCRIBE_URL, {
460
578
  method: "POST",
461
579
  body: form,
@@ -550,288 +668,339 @@ export function useVoiceDictation(
550
668
  }
551
669
  }
552
670
  },
553
- [startMeter, startTimer, teardown, failWith],
671
+ [startMeter, startTimer, teardown, failWith, getVoiceContextPack],
554
672
  );
555
673
 
556
- const startBrowser = useCallback(async () => {
557
- const Ctor = getSpeechRecognitionCtor();
558
- if (!Ctor) {
559
- throw new Error(
560
- "Your browser doesn't support speech recognition. Add an OpenAI API key in settings for Whisper transcription.",
561
- );
562
- }
563
- // Still request mic to drive the amplitude meter, so the UI doesn't look
564
- // dead while the user talks. SpeechRecognition manages its own capture
565
- // under the hood in most browsers.
566
- let stream: MediaStream | null = null;
567
- try {
568
- stream = await navigator.mediaDevices.getUserMedia({ audio: true });
569
- mediaStreamRef.current = stream;
570
- startMeter(stream);
571
- } catch {
572
- /* non-fatal — recognition can still work without our analyser */
573
- }
574
-
575
- if (cancelledRef.current) {
576
- if (stream) for (const track of stream.getTracks()) track.stop();
577
- mediaStreamRef.current = null;
578
- cancelledRef.current = false;
579
- setState("idle");
580
- return;
581
- }
582
-
583
- const recognition = new Ctor();
584
- recognition.continuous = true;
585
- recognition.interimResults = true;
586
- recognition.lang =
587
- (typeof navigator !== "undefined" && navigator.language) || "en-US";
588
- speechRef.current = recognition;
589
- speechTranscriptRef.current = "";
590
-
591
- recognition.onresult = (event: any) => {
592
- let interim = "";
593
- for (let i = event.resultIndex; i < event.results.length; i++) {
594
- const result = event.results[i];
595
- const text = result[0]?.transcript ?? "";
596
- if (result.isFinal) {
597
- speechTranscriptRef.current += text;
598
- } else {
599
- interim += text;
600
- }
674
+ const startBrowser = useCallback(
675
+ async (prefs: VoicePrefs) => {
676
+ const Ctor = getSpeechRecognitionCtor();
677
+ if (!Ctor) {
678
+ throw new Error(
679
+ "Your browser doesn't support speech recognition. Add an OpenAI API key in settings for Whisper transcription.",
680
+ );
681
+ }
682
+ // Still request mic to drive the amplitude meter, so the UI doesn't look
683
+ // dead while the user talks. SpeechRecognition manages its own capture
684
+ // under the hood in most browsers.
685
+ let stream: MediaStream | null = null;
686
+ try {
687
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
688
+ mediaStreamRef.current = stream;
689
+ startMeter(stream);
690
+ } catch {
691
+ /* non-fatal — recognition can still work without our analyser */
601
692
  }
602
- onLiveUpdateRef.current?.(speechTranscriptRef.current, interim);
603
- };
604
- recognition.onerror = (event: any) => {
605
- if (event?.error === "no-speech" || event?.error === "aborted") return;
606
- failWith(voiceDictationSpeechErrorMessage(event?.error));
607
- };
608
- recognition.onend = () => {
609
- const text = speechTranscriptRef.current.trim();
610
- const wasCancelled = cancelledRef.current;
611
- cancelledRef.current = false;
612
- teardown();
613
- if (!wasCancelled && text) onTranscriptRef.current?.(text);
614
- setState("idle");
615
- };
616
-
617
- startTimer();
618
- setState("recording");
619
- recognition.start();
620
- }, [startMeter, startTimer, teardown, failWith]);
621
-
622
- const startGoogleRealtime = useCallback(async () => {
623
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
624
- if (cancelledRef.current) {
625
- for (const track of stream.getTracks()) track.stop();
626
- cancelledRef.current = false;
627
- setState("idle");
628
- return;
629
- }
630
-
631
- const sessionRes = await fetch(GOOGLE_REALTIME_SESSION_URL, {
632
- method: "POST",
633
- headers: { "Content-Type": "application/json" },
634
- body: JSON.stringify({
635
- language:
636
- (typeof navigator !== "undefined" && navigator.language) || "en-US",
637
- }),
638
- });
639
- const sessionBody = (await sessionRes
640
- .json()
641
- .catch(() => ({ error: `HTTP ${sessionRes.status}` }))) as {
642
- websocketUrl?: string;
643
- websocketProtocol?: string;
644
- sessionToken?: string;
645
- error?: string;
646
- };
647
- if (
648
- !sessionRes.ok ||
649
- !sessionBody.websocketUrl ||
650
- !sessionBody.sessionToken
651
- ) {
652
- for (const track of stream.getTracks()) track.stop();
653
- throw new Error(
654
- sessionBody.error ||
655
- `Could not start Google realtime transcription (${sessionRes.status})`,
656
- );
657
- }
658
- if (cancelledRef.current) {
659
- for (const track of stream.getTracks()) track.stop();
660
- cancelledRef.current = false;
661
- setState("idle");
662
- return;
663
- }
664
-
665
- mediaStreamRef.current = stream;
666
- const mimeType = pickMimeType();
667
- const recorder = new MediaRecorder(stream, { mimeType });
668
- mediaRecorderRef.current = recorder;
669
- realtimeFinalRef.current = "";
670
- realtimeInterimRef.current = "";
671
693
 
672
- let finalized = false;
673
- let recorderStarted = false;
674
- const finish = (errorMessage?: string) => {
675
- if (finalized) return;
676
- finalized = true;
677
- const finalText = realtimeFinalRef.current.trim();
678
- const interimText = realtimeInterimRef.current.trim();
679
- const text = [finalText, interimText].filter(Boolean).join(" ").trim();
680
- teardown();
681
694
  if (cancelledRef.current) {
695
+ if (stream) for (const track of stream.getTracks()) track.stop();
696
+ mediaStreamRef.current = null;
682
697
  cancelledRef.current = false;
683
698
  setState("idle");
684
699
  return;
685
700
  }
686
- if (text) {
687
- onTranscriptRef.current?.(text);
688
- setState("idle");
689
- return;
690
- }
691
- if (errorMessage) {
692
- failWith(errorMessage);
693
- return;
694
- }
695
- setState("idle");
696
- };
697
-
698
- recorder.ondataavailable = async (event) => {
699
- if (!event.data || event.data.size === 0) return;
700
- const socket = realtimeSocketRef.current;
701
- if (!socket || socket.readyState !== WebSocket.OPEN) return;
702
- try {
703
- socket.send(await event.data.arrayBuffer());
704
- } catch (err) {
705
- finish((err as Error)?.message ?? "Realtime audio upload failed");
706
- }
707
- };
708
701
 
709
- recorder.onstop = () => {
710
- const socket = realtimeSocketRef.current;
711
- if (!socket || socket.readyState !== WebSocket.OPEN) {
712
- finish();
713
- return;
714
- }
715
- setState("transcribing");
716
- try {
717
- socket.send(JSON.stringify({ type: "stop" }));
718
- } catch {
719
- finish();
720
- return;
721
- }
722
- realtimeStopTimeoutRef.current = window.setTimeout(() => {
723
- const activeSocket = realtimeSocketRef.current;
724
- if (activeSocket?.readyState === WebSocket.OPEN) {
725
- try {
726
- activeSocket.close();
727
- } catch {
728
- finish();
702
+ const recognition = new Ctor();
703
+ recognition.continuous = true;
704
+ recognition.interimResults = true;
705
+ recognition.lang =
706
+ (typeof navigator !== "undefined" && navigator.language) || "en-US";
707
+ speechRef.current = recognition;
708
+ speechTranscriptRef.current = "";
709
+
710
+ recognition.onresult = (event: any) => {
711
+ let interim = "";
712
+ for (let i = event.resultIndex; i < event.results.length; i++) {
713
+ const result = event.results[i];
714
+ const text = result[0]?.transcript ?? "";
715
+ if (result.isFinal) {
716
+ speechTranscriptRef.current += text;
717
+ } else {
718
+ interim += text;
729
719
  }
720
+ }
721
+ onLiveUpdateRef.current?.(speechTranscriptRef.current, interim);
722
+ };
723
+ recognition.onerror = (event: any) => {
724
+ if (event?.error === "no-speech" || event?.error === "aborted") return;
725
+ failWith(voiceDictationSpeechErrorMessage(event?.error));
726
+ };
727
+ recognition.onend = () => {
728
+ const text = speechTranscriptRef.current.trim();
729
+ const wasCancelled = cancelledRef.current;
730
+ cancelledRef.current = false;
731
+ teardown();
732
+ if (wasCancelled || !text) {
733
+ setState("idle");
730
734
  return;
731
735
  }
732
- finish();
733
- }, 10000);
734
- };
736
+ void (async () => {
737
+ let finalText = text;
738
+ if (prefs.cleanupEnabled) {
739
+ setState("transcribing");
740
+ try {
741
+ finalText = await cleanupRecognizedText({
742
+ text,
743
+ provider: prefs.provider,
744
+ instructions: prefs.instructions,
745
+ contextPack: await getVoiceContextPack(),
746
+ });
747
+ } catch {
748
+ finalText = text;
749
+ }
750
+ }
751
+ if (!cancelledRef.current && finalText) {
752
+ onTranscriptRef.current?.(finalText);
753
+ }
754
+ cancelledRef.current = false;
755
+ setState("idle");
756
+ })();
757
+ };
735
758
 
736
- const socket = new WebSocket(sessionBody.websocketUrl, [
737
- sessionBody.websocketProtocol || GOOGLE_REALTIME_WS_PROTOCOL,
738
- sessionBody.sessionToken,
739
- ]);
740
- socket.binaryType = "arraybuffer";
741
- realtimeSocketRef.current = socket;
759
+ startTimer();
760
+ setState("recording");
761
+ recognition.start();
762
+ },
763
+ [startMeter, startTimer, teardown, failWith, getVoiceContextPack],
764
+ );
742
765
 
743
- socket.onmessage = (event) => {
766
+ const startGoogleRealtime = useCallback(
767
+ async (prefs: VoicePrefs) => {
768
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
744
769
  if (cancelledRef.current) {
745
- finish();
746
- return;
747
- }
748
- const raw =
749
- typeof event.data === "string"
750
- ? event.data
751
- : event.data instanceof Blob
752
- ? null
753
- : new TextDecoder().decode(event.data);
754
- if (!raw) return;
755
- let message:
756
- | {
757
- type?: string;
758
- text?: string;
759
- error?: string;
760
- }
761
- | undefined;
762
- try {
763
- message = JSON.parse(raw);
764
- } catch {
765
- return;
766
- }
767
- if (!message?.type) return;
768
- if (message.type === "ready") {
769
- if (!recorderStarted) {
770
- recorderStarted = true;
771
- startMeter(stream);
772
- startTimer();
773
- setState("recording");
774
- recorder.start(100);
775
- }
770
+ for (const track of stream.getTracks()) track.stop();
771
+ cancelledRef.current = false;
772
+ setState("idle");
776
773
  return;
777
774
  }
778
- if (message.type === "partial") {
779
- realtimeInterimRef.current = (message.text ?? "").trim();
780
- onLiveUpdateRef.current?.(
781
- realtimeFinalRef.current,
782
- realtimeInterimRef.current,
775
+
776
+ const sessionRes = await fetch(GOOGLE_REALTIME_SESSION_URL, {
777
+ method: "POST",
778
+ headers: { "Content-Type": "application/json" },
779
+ body: JSON.stringify({
780
+ language:
781
+ (typeof navigator !== "undefined" && navigator.language) || "en-US",
782
+ }),
783
+ });
784
+ const sessionBody = (await sessionRes
785
+ .json()
786
+ .catch(() => ({ error: `HTTP ${sessionRes.status}` }))) as {
787
+ websocketUrl?: string;
788
+ websocketProtocol?: string;
789
+ sessionToken?: string;
790
+ error?: string;
791
+ };
792
+ if (
793
+ !sessionRes.ok ||
794
+ !sessionBody.websocketUrl ||
795
+ !sessionBody.sessionToken
796
+ ) {
797
+ for (const track of stream.getTracks()) track.stop();
798
+ throw new Error(
799
+ sessionBody.error ||
800
+ `Could not start Google realtime transcription (${sessionRes.status})`,
783
801
  );
802
+ }
803
+ if (cancelledRef.current) {
804
+ for (const track of stream.getTracks()) track.stop();
805
+ cancelledRef.current = false;
806
+ setState("idle");
784
807
  return;
785
808
  }
786
- if (message.type === "final") {
787
- const next = (message.text ?? "").trim();
788
- if (next) {
789
- realtimeFinalRef.current = [realtimeFinalRef.current.trim(), next]
809
+
810
+ mediaStreamRef.current = stream;
811
+ const mimeType = pickMimeType();
812
+ const recorder = new MediaRecorder(stream, { mimeType });
813
+ mediaRecorderRef.current = recorder;
814
+ realtimeFinalRef.current = "";
815
+ realtimeInterimRef.current = "";
816
+
817
+ let finalized = false;
818
+ let recorderStarted = false;
819
+ const finish = (errorMessage?: string) => {
820
+ if (finalized) return;
821
+ finalized = true;
822
+ void (async () => {
823
+ const finalText = realtimeFinalRef.current.trim();
824
+ const interimText = realtimeInterimRef.current.trim();
825
+ const text = [finalText, interimText]
790
826
  .filter(Boolean)
791
- .join(" ");
827
+ .join(" ")
828
+ .trim();
829
+ teardown();
830
+ if (cancelledRef.current) {
831
+ cancelledRef.current = false;
832
+ setState("idle");
833
+ return;
834
+ }
835
+ if (text) {
836
+ let cleanedText = text;
837
+ if (prefs.cleanupEnabled) {
838
+ setState("transcribing");
839
+ try {
840
+ cleanedText = await cleanupRecognizedText({
841
+ text,
842
+ provider: prefs.provider,
843
+ instructions: prefs.instructions,
844
+ contextPack: await getVoiceContextPack(),
845
+ });
846
+ } catch {
847
+ cleanedText = text;
848
+ }
849
+ }
850
+ if (!cancelledRef.current && cleanedText) {
851
+ onTranscriptRef.current?.(cleanedText);
852
+ }
853
+ cancelledRef.current = false;
854
+ setState("idle");
855
+ return;
856
+ }
857
+ if (errorMessage) {
858
+ failWith(errorMessage);
859
+ return;
860
+ }
861
+ setState("idle");
862
+ })();
863
+ };
864
+
865
+ recorder.ondataavailable = async (event) => {
866
+ if (!event.data || event.data.size === 0) return;
867
+ const socket = realtimeSocketRef.current;
868
+ if (!socket || socket.readyState !== WebSocket.OPEN) return;
869
+ try {
870
+ socket.send(await event.data.arrayBuffer());
871
+ } catch (err) {
872
+ finish((err as Error)?.message ?? "Realtime audio upload failed");
792
873
  }
793
- realtimeInterimRef.current = "";
794
- onLiveUpdateRef.current?.(realtimeFinalRef.current, "");
795
- return;
796
- }
797
- if (message.type === "error") {
798
- finish(message.error || "Google realtime transcription failed");
799
- return;
800
- }
801
- if (message.type === "end") {
802
- finish();
803
- }
804
- };
874
+ };
805
875
 
806
- socket.onerror = () => {
807
- finish("Google realtime transcription connection failed");
808
- };
876
+ recorder.onstop = () => {
877
+ const socket = realtimeSocketRef.current;
878
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
879
+ finish();
880
+ return;
881
+ }
882
+ setState("transcribing");
883
+ try {
884
+ socket.send(JSON.stringify({ type: "stop" }));
885
+ } catch {
886
+ finish();
887
+ return;
888
+ }
889
+ realtimeStopTimeoutRef.current = window.setTimeout(() => {
890
+ const activeSocket = realtimeSocketRef.current;
891
+ if (activeSocket?.readyState === WebSocket.OPEN) {
892
+ try {
893
+ activeSocket.close();
894
+ } catch {
895
+ finish();
896
+ }
897
+ return;
898
+ }
899
+ finish();
900
+ }, 10000);
901
+ };
809
902
 
810
- socket.onclose = () => {
811
- finish();
812
- };
903
+ const socket = new WebSocket(sessionBody.websocketUrl, [
904
+ sessionBody.websocketProtocol || GOOGLE_REALTIME_WS_PROTOCOL,
905
+ sessionBody.sessionToken,
906
+ ]);
907
+ socket.binaryType = "arraybuffer";
908
+ realtimeSocketRef.current = socket;
813
909
 
814
- socket.onopen = () => {
815
- if (cancelledRef.current) {
910
+ socket.onmessage = (event) => {
911
+ if (cancelledRef.current) {
912
+ finish();
913
+ return;
914
+ }
915
+ const raw =
916
+ typeof event.data === "string"
917
+ ? event.data
918
+ : event.data instanceof Blob
919
+ ? null
920
+ : new TextDecoder().decode(event.data);
921
+ if (!raw) return;
922
+ let message:
923
+ | {
924
+ type?: string;
925
+ text?: string;
926
+ error?: string;
927
+ }
928
+ | undefined;
929
+ try {
930
+ message = JSON.parse(raw);
931
+ } catch {
932
+ return;
933
+ }
934
+ if (!message?.type) return;
935
+ if (message.type === "ready") {
936
+ if (!recorderStarted) {
937
+ recorderStarted = true;
938
+ startMeter(stream);
939
+ startTimer();
940
+ setState("recording");
941
+ recorder.start(100);
942
+ }
943
+ return;
944
+ }
945
+ if (message.type === "partial") {
946
+ realtimeInterimRef.current = (message.text ?? "").trim();
947
+ onLiveUpdateRef.current?.(
948
+ realtimeFinalRef.current,
949
+ realtimeInterimRef.current,
950
+ );
951
+ return;
952
+ }
953
+ if (message.type === "final") {
954
+ const next = (message.text ?? "").trim();
955
+ if (next) {
956
+ realtimeFinalRef.current = [realtimeFinalRef.current.trim(), next]
957
+ .filter(Boolean)
958
+ .join(" ");
959
+ }
960
+ realtimeInterimRef.current = "";
961
+ onLiveUpdateRef.current?.(realtimeFinalRef.current, "");
962
+ return;
963
+ }
964
+ if (message.type === "error") {
965
+ finish(message.error || "Google realtime transcription failed");
966
+ return;
967
+ }
968
+ if (message.type === "end") {
969
+ finish();
970
+ }
971
+ };
972
+
973
+ socket.onerror = () => {
974
+ finish("Google realtime transcription connection failed");
975
+ };
976
+
977
+ socket.onclose = () => {
816
978
  finish();
817
- return;
818
- }
819
- try {
820
- socket.send(
821
- JSON.stringify({
822
- type: "start",
823
- language:
824
- (typeof navigator !== "undefined" && navigator.language) ||
825
- "en-US",
826
- interimResults: true,
827
- mimeType,
828
- }),
829
- );
830
- } catch (err) {
831
- finish((err as Error)?.message ?? "Could not start realtime stream");
832
- }
833
- };
834
- }, [startMeter, startTimer, teardown, failWith]);
979
+ };
980
+
981
+ socket.onopen = () => {
982
+ if (cancelledRef.current) {
983
+ finish();
984
+ return;
985
+ }
986
+ try {
987
+ socket.send(
988
+ JSON.stringify({
989
+ type: "start",
990
+ language:
991
+ (typeof navigator !== "undefined" && navigator.language) ||
992
+ "en-US",
993
+ interimResults: true,
994
+ mimeType,
995
+ }),
996
+ );
997
+ } catch (err) {
998
+ finish((err as Error)?.message ?? "Could not start realtime stream");
999
+ }
1000
+ };
1001
+ },
1002
+ [startMeter, startTimer, teardown, failWith, getVoiceContextPack],
1003
+ );
835
1004
 
836
1005
  const start = useCallback(async () => {
837
1006
  if (state === "recording" || state === "starting") return;
@@ -875,9 +1044,9 @@ export function useVoiceDictation(
875
1044
  "Your browser doesn't support audio recording, so Google realtime transcription can't start.",
876
1045
  );
877
1046
  }
878
- await startGoogleRealtime();
1047
+ await startGoogleRealtime(prefs);
879
1048
  } else {
880
- await startBrowser();
1049
+ await startBrowser(prefs);
881
1050
  }
882
1051
  } catch (err) {
883
1052
  if (cancelledRef.current) {