@opengeni/react 1.4.0 → 1.4.4

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 (218) hide show
  1. package/README.md +8 -0
  2. package/dist/accounts.js +2 -0
  3. package/dist/accounts.js.map +1 -1
  4. package/dist/artifacts-document.js +5 -4
  5. package/dist/artifacts-presentation.js +5 -4
  6. package/dist/artifacts-spreadsheet.js +4 -3
  7. package/dist/artifacts.js +14 -13
  8. package/dist/artifacts.js.map +1 -1
  9. package/dist/browser-viewer-5EEAS3IU.js +17 -0
  10. package/dist/chat-interactive-block-ZFHOKTMS.js +12 -0
  11. package/dist/{chunk-WEHMPOXK.js → chunk-2DDJ6YH6.js} +3 -1
  12. package/dist/{chunk-WEHMPOXK.js.map → chunk-2DDJ6YH6.js.map} +1 -1
  13. package/dist/{chunk-C5X5AJXK.js → chunk-2HVLQ26H.js} +5 -4
  14. package/dist/{chunk-C5X5AJXK.js.map → chunk-2HVLQ26H.js.map} +1 -1
  15. package/dist/{chunk-HDJWDK7W.js → chunk-2OQA3CPB.js} +300 -166
  16. package/dist/{chunk-HDJWDK7W.js.map → chunk-2OQA3CPB.js.map} +1 -1
  17. package/dist/{chunk-A4UJB2MM.js → chunk-32CGLXW2.js} +4 -3
  18. package/dist/{chunk-A4UJB2MM.js.map → chunk-32CGLXW2.js.map} +1 -1
  19. package/dist/{chunk-5CVXZA3Y.js → chunk-45NUOSXQ.js} +3 -2
  20. package/dist/{chunk-5CVXZA3Y.js.map → chunk-45NUOSXQ.js.map} +1 -1
  21. package/dist/{chunk-WUISZ3WU.js → chunk-53KJBGPZ.js} +5 -4
  22. package/dist/{chunk-WUISZ3WU.js.map → chunk-53KJBGPZ.js.map} +1 -1
  23. package/dist/{chunk-MBMPHOMA.js → chunk-55DXD22O.js} +3 -2
  24. package/dist/{chunk-MBMPHOMA.js.map → chunk-55DXD22O.js.map} +1 -1
  25. package/dist/{chunk-JT4JW7BC.js → chunk-5I4VRWBB.js} +3 -1
  26. package/dist/{chunk-JT4JW7BC.js.map → chunk-5I4VRWBB.js.map} +1 -1
  27. package/dist/{chunk-UK4TINKT.js → chunk-B27E3V37.js} +3 -1
  28. package/dist/{chunk-UK4TINKT.js.map → chunk-B27E3V37.js.map} +1 -1
  29. package/dist/{chunk-J6DBS4T3.js → chunk-BNKV5ZLF.js} +5 -4
  30. package/dist/{chunk-J6DBS4T3.js.map → chunk-BNKV5ZLF.js.map} +1 -1
  31. package/dist/{chunk-YM2DE7BQ.js → chunk-C5BF4L26.js} +13 -12
  32. package/dist/{chunk-YM2DE7BQ.js.map → chunk-C5BF4L26.js.map} +1 -1
  33. package/dist/{chunk-YBXQDQFX.js → chunk-C7CMD56T.js} +6 -5
  34. package/dist/{chunk-YBXQDQFX.js.map → chunk-C7CMD56T.js.map} +1 -1
  35. package/dist/{chunk-UGLEYEMP.js → chunk-CM7LVUKC.js} +3 -1
  36. package/dist/{chunk-UGLEYEMP.js.map → chunk-CM7LVUKC.js.map} +1 -1
  37. package/dist/{chunk-JVYQRRC4.js → chunk-CVYCLFMH.js} +3 -1
  38. package/dist/{chunk-JVYQRRC4.js.map → chunk-CVYCLFMH.js.map} +1 -1
  39. package/dist/{chunk-LJ2UTJJE.js → chunk-EAOSEIXB.js} +182 -97
  40. package/dist/chunk-EAOSEIXB.js.map +1 -0
  41. package/dist/{chunk-Y2PRMYLC.js → chunk-G3U2A6GS.js} +3 -2
  42. package/dist/{chunk-Y2PRMYLC.js.map → chunk-G3U2A6GS.js.map} +1 -1
  43. package/dist/{chunk-WZMYFJD3.js → chunk-G6YRCIYJ.js} +16 -7
  44. package/dist/{chunk-WZMYFJD3.js.map → chunk-G6YRCIYJ.js.map} +1 -1
  45. package/dist/{chunk-B3V6TG77.js → chunk-GC3PH6N7.js} +3 -1
  46. package/dist/{chunk-B3V6TG77.js.map → chunk-GC3PH6N7.js.map} +1 -1
  47. package/dist/{chunk-CJUO7J5Z.js → chunk-HXYOSSQO.js} +3 -2
  48. package/dist/{chunk-CJUO7J5Z.js.map → chunk-HXYOSSQO.js.map} +1 -1
  49. package/dist/{chunk-GYVRFZDY.js → chunk-I6OY3MWC.js} +310 -115
  50. package/dist/chunk-I6OY3MWC.js.map +1 -0
  51. package/dist/{chunk-BRM6FVZA.js → chunk-IEE7WCLN.js} +4 -3
  52. package/dist/{chunk-BRM6FVZA.js.map → chunk-IEE7WCLN.js.map} +1 -1
  53. package/dist/{chunk-DYWBUX5Q.js → chunk-IMSUVYQB.js} +6 -5
  54. package/dist/{chunk-DYWBUX5Q.js.map → chunk-IMSUVYQB.js.map} +1 -1
  55. package/dist/{chunk-LN6TCXD3.js → chunk-IUHHPMAX.js} +184 -48
  56. package/dist/chunk-IUHHPMAX.js.map +1 -0
  57. package/dist/{chunk-SGKD2OCN.js → chunk-IYQCWGU5.js} +6 -5
  58. package/dist/{chunk-SGKD2OCN.js.map → chunk-IYQCWGU5.js.map} +1 -1
  59. package/dist/{chunk-J2QWVSQY.js → chunk-JH7V627O.js} +8 -7
  60. package/dist/{chunk-J2QWVSQY.js.map → chunk-JH7V627O.js.map} +1 -1
  61. package/dist/{chunk-EESD3PY3.js → chunk-JHTIMBOL.js} +6 -5
  62. package/dist/{chunk-EESD3PY3.js.map → chunk-JHTIMBOL.js.map} +1 -1
  63. package/dist/{chunk-LNIRAOA3.js → chunk-L2J3PU7A.js} +7 -6
  64. package/dist/{chunk-LNIRAOA3.js.map → chunk-L2J3PU7A.js.map} +1 -1
  65. package/dist/{chunk-ZYRGEKWS.js → chunk-LJUMYUBO.js} +3 -1
  66. package/dist/{chunk-ZYRGEKWS.js.map → chunk-LJUMYUBO.js.map} +1 -1
  67. package/dist/{chunk-4RVYOY4O.js → chunk-LSW2OJVV.js} +4 -3
  68. package/dist/{chunk-4RVYOY4O.js.map → chunk-LSW2OJVV.js.map} +1 -1
  69. package/dist/{chunk-UTRTG6LH.js → chunk-LVS3MH5T.js} +3 -1
  70. package/dist/{chunk-UTRTG6LH.js.map → chunk-LVS3MH5T.js.map} +1 -1
  71. package/dist/chunk-M357MHH3.js +2 -0
  72. package/dist/{chunk-WRHIZYZF.js → chunk-MEQM4LI3.js} +6 -5
  73. package/dist/{chunk-WRHIZYZF.js.map → chunk-MEQM4LI3.js.map} +1 -1
  74. package/dist/chunk-NX4ADH73.js +14 -0
  75. package/dist/chunk-NX4ADH73.js.map +1 -0
  76. package/dist/{chunk-XCHWDJ22.js → chunk-OGTTUYNT.js} +3 -1
  77. package/dist/{chunk-XCHWDJ22.js.map → chunk-OGTTUYNT.js.map} +1 -1
  78. package/dist/{chunk-GIMRELBW.js → chunk-OYQQJL3J.js} +3 -1
  79. package/dist/{chunk-GIMRELBW.js.map → chunk-OYQQJL3J.js.map} +1 -1
  80. package/dist/{chunk-RSI7XYXL.js → chunk-PBCZL4JP.js} +6 -5
  81. package/dist/{chunk-RSI7XYXL.js.map → chunk-PBCZL4JP.js.map} +1 -1
  82. package/dist/{chunk-I7IWRQLN.js → chunk-PHBFJCEU.js} +17 -11
  83. package/dist/chunk-PHBFJCEU.js.map +1 -0
  84. package/dist/{chunk-3CTRFMAK.js → chunk-PKCAYHDC.js} +5 -4
  85. package/dist/{chunk-3CTRFMAK.js.map → chunk-PKCAYHDC.js.map} +1 -1
  86. package/dist/{chunk-K3HQAKQX.js → chunk-PMV53IMM.js} +3 -1
  87. package/dist/{chunk-K3HQAKQX.js.map → chunk-PMV53IMM.js.map} +1 -1
  88. package/dist/{chunk-YQEAA6LT.js → chunk-QHUE7E4N.js} +3 -2
  89. package/dist/{chunk-YQEAA6LT.js.map → chunk-QHUE7E4N.js.map} +1 -1
  90. package/dist/{chunk-OA6HFRQP.js → chunk-QMMCJGFI.js} +4 -3
  91. package/dist/{chunk-OA6HFRQP.js.map → chunk-QMMCJGFI.js.map} +1 -1
  92. package/dist/{chunk-YV4L3ILF.js → chunk-QV3DJVRZ.js} +3 -1
  93. package/dist/{chunk-YV4L3ILF.js.map → chunk-QV3DJVRZ.js.map} +1 -1
  94. package/dist/{chunk-447JLCBU.js → chunk-S2QH6EZ5.js} +3 -1
  95. package/dist/{chunk-447JLCBU.js.map → chunk-S2QH6EZ5.js.map} +1 -1
  96. package/dist/{chunk-PNY3JKBJ.js → chunk-SFH43JT2.js} +5 -4
  97. package/dist/{chunk-PNY3JKBJ.js.map → chunk-SFH43JT2.js.map} +1 -1
  98. package/dist/{chunk-AWZLIMKX.js → chunk-T46QZH3E.js} +3 -1
  99. package/dist/{chunk-AWZLIMKX.js.map → chunk-T46QZH3E.js.map} +1 -1
  100. package/dist/{chunk-5J4FOW6Q.js → chunk-TNEZNMSN.js} +3 -1
  101. package/dist/{chunk-5J4FOW6Q.js.map → chunk-TNEZNMSN.js.map} +1 -1
  102. package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
  103. package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
  104. package/dist/{chunk-H5X55XH7.js → chunk-UDPH5SLV.js} +4 -3
  105. package/dist/{chunk-H5X55XH7.js.map → chunk-UDPH5SLV.js.map} +1 -1
  106. package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
  107. package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
  108. package/dist/{chunk-5F4WR7X7.js → chunk-XS67YG5R.js} +3 -1
  109. package/dist/{chunk-5F4WR7X7.js.map → chunk-XS67YG5R.js.map} +1 -1
  110. package/dist/{chunk-TKOTKRWY.js → chunk-XTD7TZHW.js} +3 -1
  111. package/dist/{chunk-TKOTKRWY.js.map → chunk-XTD7TZHW.js.map} +1 -1
  112. package/dist/{chunk-A2PI6UMY.js → chunk-YSRBFJMQ.js} +3 -1
  113. package/dist/{chunk-A2PI6UMY.js.map → chunk-YSRBFJMQ.js.map} +1 -1
  114. package/dist/chunk-YVFRBXST.js +1420 -0
  115. package/dist/chunk-YVFRBXST.js.map +1 -0
  116. package/dist/{chunk-SCOTPE2I.js → chunk-YYLBXXTE.js} +3 -1
  117. package/dist/{chunk-SCOTPE2I.js.map → chunk-YYLBXXTE.js.map} +1 -1
  118. package/dist/{chunk-RHREZ7BO.js → chunk-ZUWF4NF7.js} +3 -1
  119. package/dist/{chunk-RHREZ7BO.js.map → chunk-ZUWF4NF7.js.map} +1 -1
  120. package/dist/clipboard.js +3 -2
  121. package/dist/components/composer-transcription-control.d.ts +4 -0
  122. package/dist/components/session-conversation.d.ts +6 -0
  123. package/dist/composer-voice-events.d.ts +8 -0
  124. package/dist/composer.js +15 -13
  125. package/dist/computer-viewer-ELNLZGHI.js +20 -0
  126. package/dist/connect.js +15 -12
  127. package/dist/connect.js.map +1 -1
  128. package/dist/desktop.js +6 -5
  129. package/dist/desktop.js.map +1 -1
  130. package/dist/diffs.js +2 -1
  131. package/dist/diffs.js.map +1 -1
  132. package/dist/editor.js +4 -3
  133. package/dist/editor.js.map +1 -1
  134. package/dist/embedded-voice-NIL4RDX3.js +69 -0
  135. package/dist/embedded-voice-NIL4RDX3.js.map +1 -0
  136. package/dist/{fleet-decision-row-YZ3ZKWMM.js → fleet-decision-row-SI63O5V4.js} +3 -1
  137. package/dist/{fleet-decision-row-YZ3ZKWMM.js.map → fleet-decision-row-SI63O5V4.js.map} +1 -1
  138. package/dist/hooks/use-client-config-flags.d.ts +6 -0
  139. package/dist/hooks/use-computer-session.d.ts +2 -0
  140. package/dist/hooks/use-realtime-voice-models.d.ts +9 -0
  141. package/dist/hooks/use-voice-input.d.ts +8 -1
  142. package/dist/index.js +47 -45
  143. package/dist/index.js.map +1 -1
  144. package/dist/interaction.js +14 -13
  145. package/dist/interaction.js.map +1 -1
  146. package/dist/{latest-question-GPYM2JNU.js → latest-question-2POFPYNB.js} +5 -4
  147. package/dist/{latest-question-GPYM2JNU.js.map → latest-question-2POFPYNB.js.map} +1 -1
  148. package/dist/machines.js +8 -7
  149. package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-DQ4DRNVT.js} +3 -1
  150. package/dist/{markdown-table-layout-QXSLK4BY.js.map → markdown-table-layout-DQ4DRNVT.js.map} +1 -1
  151. package/dist/model-policy-picker-menu-WRNSRFQC.js +15 -0
  152. package/dist/model-policy.js +2 -1
  153. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
  154. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
  155. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
  156. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
  157. package/dist/{platform-activity-row-627BFZ46.js → platform-activity-row-PJJVATTZ.js} +3 -1
  158. package/dist/{platform-activity-row-627BFZ46.js.map → platform-activity-row-PJJVATTZ.js.map} +1 -1
  159. package/dist/{queue-surface-implementation-ZHFI5JBP.js → queue-surface-implementation-67FS7FUI.js} +10 -9
  160. package/dist/{queue-surface-implementation-ZHFI5JBP.js.map → queue-surface-implementation-67FS7FUI.js.map} +1 -1
  161. package/dist/realtime/embedded-voice.d.ts +20 -0
  162. package/dist/realtime.js +22 -1353
  163. package/dist/realtime.js.map +1 -1
  164. package/dist/sandbox-files-34CJJNOY.js +21 -0
  165. package/dist/session-ui.js +32 -30
  166. package/dist/session-ui.js.map +1 -1
  167. package/dist/session.js +10 -9
  168. package/dist/sites.js +2 -1
  169. package/dist/terminal.js +6 -5
  170. package/dist/terminal.js.map +1 -1
  171. package/dist/{timeline-annotation-markers-GNDHTGBD.js → timeline-annotation-markers-U3G66KUD.js} +5 -4
  172. package/dist/{timeline-annotation-markers-GNDHTGBD.js.map → timeline-annotation-markers-U3G66KUD.js.map} +1 -1
  173. package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-BI7QTPTZ.js} +4 -3
  174. package/dist/{timeline-annotation-selection-DMLMM2TX.js.map → timeline-annotation-selection-BI7QTPTZ.js.map} +1 -1
  175. package/dist/{timeline-annotations-dialog-ELRDBHTS.js → timeline-annotations-dialog-FOMHSDTE.js} +7 -6
  176. package/dist/{timeline-annotations-dialog-ELRDBHTS.js.map → timeline-annotations-dialog-FOMHSDTE.js.map} +1 -1
  177. package/dist/usage.js +6 -5
  178. package/dist/usage.js.map +1 -1
  179. package/dist/voice-recording-owner.d.ts +7 -0
  180. package/package.json +3 -3
  181. package/src/components/composer-transcription-control.tsx +109 -27
  182. package/src/components/computer-viewer.tsx +139 -30
  183. package/src/components/markdown.tsx +7 -2
  184. package/src/components/message-timeline.tsx +10 -1
  185. package/src/components/session-conversation.tsx +44 -0
  186. package/src/components/session-list.tsx +1 -1
  187. package/src/components/session-proxy-scope.tsx +26 -5
  188. package/src/composer-voice-events.ts +12 -0
  189. package/src/connection-discovery.tsx +4 -1
  190. package/src/hooks/use-browser-session.ts +445 -168
  191. package/src/hooks/use-client-config-flags.ts +8 -0
  192. package/src/hooks/use-computer-session.ts +334 -98
  193. package/src/hooks/use-goal.ts +12 -1
  194. package/src/hooks/use-realtime-voice-models.ts +67 -0
  195. package/src/hooks/use-voice-input.ts +109 -18
  196. package/src/realtime/embedded-voice.tsx +81 -0
  197. package/src/realtime/realtime-control.tsx +82 -17
  198. package/src/voice-recording-owner.ts +22 -0
  199. package/styles/compiled.css +77 -18
  200. package/styles/index.css +49 -0
  201. package/styles/responsive.css +61 -4
  202. package/styles/session-capability.css +1 -1
  203. package/dist/browser-viewer-SHYKQRNX.js +0 -16
  204. package/dist/chat-interactive-block-UGBTNB3L.js +0 -11
  205. package/dist/chunk-GYVRFZDY.js.map +0 -1
  206. package/dist/chunk-I7IWRQLN.js.map +0 -1
  207. package/dist/chunk-IWIDV7SB.js +0 -1
  208. package/dist/chunk-LJ2UTJJE.js.map +0 -1
  209. package/dist/chunk-LN6TCXD3.js.map +0 -1
  210. package/dist/computer-viewer-6SRGWRUX.js +0 -19
  211. package/dist/model-policy-picker-menu-DXHUL73H.js +0 -14
  212. package/dist/sandbox-files-GORZOUUJ.js +0 -20
  213. /package/dist/{browser-viewer-SHYKQRNX.js.map → browser-viewer-5EEAS3IU.js.map} +0 -0
  214. /package/dist/{chat-interactive-block-UGBTNB3L.js.map → chat-interactive-block-ZFHOKTMS.js.map} +0 -0
  215. /package/dist/{chunk-IWIDV7SB.js.map → chunk-M357MHH3.js.map} +0 -0
  216. /package/dist/{computer-viewer-6SRGWRUX.js.map → computer-viewer-ELNLZGHI.js.map} +0 -0
  217. /package/dist/{model-policy-picker-menu-DXHUL73H.js.map → model-policy-picker-menu-WRNSRFQC.js.map} +0 -0
  218. /package/dist/{sandbox-files-GORZOUUJ.js.map → sandbox-files-34CJJNOY.js.map} +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ COMPOSER_VOICE_INPUT_START_EVENT,
3
+ composerVoiceEventScope,
4
+ } from "../composer-voice-events";
1
5
  import type {
2
6
  ClientVoiceInputConfig,
3
7
  OpenGeniClient,
@@ -35,6 +39,10 @@ export type ComposerTranscriptionMessages = {
35
39
  stop: string;
36
40
  cancel: string;
37
41
  retry: string;
42
+ /** Transcribe a recording that was stopped without transcribing or recovered. */
43
+ transcribeSaved: string;
44
+ /** Shown near the automatic stop; `{seconds}` is replaced. */
45
+ timeRemaining: string;
38
46
  pauseRetrying: string;
39
47
  requestingPermission: string;
40
48
  recording: string;
@@ -64,32 +72,34 @@ export type ComposerTranscriptionMessages = {
64
72
  const defaultMessages: ComposerTranscriptionMessages = {
65
73
  start: "Start voice input",
66
74
  stop: "Stop and transcribe",
67
- cancel: "Cancel recording",
75
+ cancel: "Stop without transcribing",
68
76
  retry: "Retry voice input",
77
+ transcribeSaved: "Transcribe saved recording",
78
+ timeRemaining: "Stops and transcribes in {seconds}s",
69
79
  pauseRetrying: "Pause automatic retry",
70
80
  requestingPermission: "Requesting microphone…",
71
- recording: "Recording. Press Escape to cancel.",
81
+ recording: "Recording. Press Escape to stop without transcribing.",
72
82
  saving: "Saving audio locally…",
73
83
  transcribing: "Transcribing…",
74
- retrying: "Recording saved. Retrying automatically…",
75
- recovered: "Recording recovered and saved locally.",
76
- recoveredTranscript: "Transcript saved locally. Check your draft before inserting.",
84
+ retrying: "Saved. Retrying transcription…",
85
+ recovered: "Recording saved on this device.",
86
+ recoveredTranscript: "Transcript saved. Insert it into your draft?",
77
87
  insertRecoveredTranscript: "Insert saved transcript",
78
88
  discardRecovered: "Discard saved recording",
79
89
  unavailableDisabled: "Voice input is unavailable while the composer is disabled.",
80
90
  unavailable: "Voice input is unavailable for this workspace.",
81
- errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
91
+ errorPermissionDenied: "Microphone access is blocked. Allow it in site settings, then try again.",
82
92
  errorNotSupported: "Voice input is not supported on this device.",
83
93
  errorUnavailable: "Voice input is not configured.",
84
94
  errorInsufficientCredits:
85
- "Voice input needs Opengeni credits. Add credits, then retry your saved recording.",
86
- errorAllowanceExhausted: "Your usage limit was reached. Your recording is saved for later.",
95
+ "Voice input needs credits. Recording saved; retry after adding credits.",
96
+ errorAllowanceExhausted: "Usage limit reached. Recording saved for later.",
87
97
  errorPolicyBlocked: "Voice input isn't allowed for this account or workspace.",
88
98
  errorTooLarge: "Recording is too large. Try a shorter message.",
89
99
  errorInvalidAudio: "The recording could not be read. Try again.",
90
100
  errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
91
- errorRetryable: "Recording is saved locally. Retry transcription when ready.",
92
- errorHandoffUncertain: "Transcript is saved. Check your draft before inserting it again.",
101
+ errorRetryable: "Saved. Transcription failed, retry when ready.",
102
+ errorHandoffUncertain: "Transcript saved. Check your draft before inserting again.",
93
103
  errorUnknown: "Voice input could not start. Try again.",
94
104
  };
95
105
 
@@ -153,12 +163,18 @@ export function ComposerTranscriptionControl({
153
163
  const capturing = status === "requesting-permission" || status === "recording";
154
164
 
155
165
  const cancelTranscription = transcription.cancel;
166
+ const discardTranscription = transcription.discard;
167
+ const hasRecoverableRecording = transcription.hasRecoverableRecording;
156
168
  useEffect(() => {
157
169
  if (!suppressed) return;
158
170
  if (status === "recording" || status === "requesting-permission") {
159
171
  cancelTranscription();
172
+ } else if (status === "error" && !hasRecoverableRecording) {
173
+ // Live voice took over: a stale start error (nothing saved) would only
174
+ // duplicate whatever live voice reports next to it.
175
+ void discardTranscription();
160
176
  }
161
- }, [cancelTranscription, status, suppressed]);
177
+ }, [cancelTranscription, discardTranscription, hasRecoverableRecording, status, suppressed]);
162
178
  const active = capturing || status === "saving" || status === "transcribing";
163
179
  const recoverable =
164
180
  transcription.hasRecoverableRecording &&
@@ -194,11 +210,22 @@ export function ComposerTranscriptionControl({
194
210
  ? (errorMessage ?? messages.errorUnknown)
195
211
  : unavailableMessage;
196
212
 
213
+ const recoveredLabel = savedTranscript
214
+ ? (errorMessage ?? messages.recoveredTranscript)
215
+ : retrying
216
+ ? messages.retrying
217
+ : status === "error"
218
+ ? (errorMessage ?? messages.errorRetryable)
219
+ : messages.recovered;
220
+
197
221
  function start(event: MouseEvent<HTMLButtonElement>) {
198
222
  if (unavailableMessage) {
199
223
  event.preventDefault();
200
224
  return;
201
225
  }
226
+ composerVoiceEventScope(event.currentTarget)?.dispatchEvent(
227
+ new Event(COMPOSER_VOICE_INPUT_START_EVENT),
228
+ );
202
229
  void transcription.start();
203
230
  }
204
231
 
@@ -220,6 +247,9 @@ export function ComposerTranscriptionControl({
220
247
  )}
221
248
  data-transcription-status={status}
222
249
  data-transcription-capturing={capturing ? "" : undefined}
250
+ data-transcription-attention={
251
+ !capturing && (recoverable || (status === "error" && errorMessage)) ? "" : undefined
252
+ }
223
253
  >
224
254
  <span className="inline-flex min-w-0 items-center gap-1.5">
225
255
  <AnimatePresence mode="popLayout" initial={false}>
@@ -235,14 +265,11 @@ export function ComposerTranscriptionControl({
235
265
  "bg-og-surface-2/70 pl-2 pr-1 pointer-coarse:h-11",
236
266
  )}
237
267
  >
238
- <span className="og-transcription-recovered-label max-w-44 truncate text-og-xs text-og-fg-muted max-sm:max-w-28">
239
- {savedTranscript
240
- ? (errorMessage ?? messages.recoveredTranscript)
241
- : retrying
242
- ? messages.retrying
243
- : status === "error"
244
- ? (errorMessage ?? messages.errorRetryable)
245
- : messages.recovered}
268
+ <span
269
+ title={recoveredLabel}
270
+ className="og-transcription-recovered-label max-w-56 line-clamp-2 text-og-xs leading-tight text-og-fg-muted max-sm:max-w-32"
271
+ >
272
+ {recoveredLabel}
246
273
  </span>
247
274
  <Tip
248
275
  tip={
@@ -250,7 +277,9 @@ export function ComposerTranscriptionControl({
250
277
  ? messages.insertRecoveredTranscript
251
278
  : retrying
252
279
  ? messages.pauseRetrying
253
- : messages.retry
280
+ : status === "recovered"
281
+ ? messages.transcribeSaved
282
+ : messages.retry
254
283
  }
255
284
  >
256
285
  <button
@@ -268,7 +297,9 @@ export function ComposerTranscriptionControl({
268
297
  ? messages.insertRecoveredTranscript
269
298
  : retrying
270
299
  ? messages.pauseRetrying
271
- : messages.retry
300
+ : status === "recovered"
301
+ ? messages.transcribeSaved
302
+ : messages.retry
272
303
  }
273
304
  className={cn(
274
305
  "inline-flex size-7 shrink-0 items-center justify-center rounded-og-sm",
@@ -330,6 +361,13 @@ export function ComposerTranscriptionControl({
330
361
  stream={status === "recording" ? transcription.stream : null}
331
362
  mode={status === "recording" ? "recording" : "transcribing"}
332
363
  />
364
+ {status === "recording" && transcription.recordingStartedAt !== null ? (
365
+ <RecordingClock
366
+ startedAt={transcription.recordingStartedAt}
367
+ maxSeconds={transcription.maxRecordingSeconds}
368
+ remainingLabel={messages.timeRemaining}
369
+ />
370
+ ) : null}
333
371
  </span>
334
372
  )}
335
373
  {status === "recording" ? (
@@ -401,14 +439,22 @@ export function ComposerTranscriptionControl({
401
439
  )}
402
440
  </AnimatePresence>
403
441
  {status === "error" && errorMessage && !recoverable ? (
404
- <Tip tip={errorMessage}>
405
- <span
406
- aria-hidden="true"
407
- className="og-transcription-error-label max-w-40 truncate text-og-xs text-og-status-failed max-sm:max-w-24"
408
- >
442
+ // Nothing is saved in this state, so dismissing loses no speech.
443
+ <button
444
+ type="button"
445
+ title={errorMessage}
446
+ aria-label={`Dismiss: ${errorMessage}`}
447
+ onClick={() => void transcription.discard()}
448
+ className={cn(
449
+ "inline-flex min-w-0 items-center gap-1 rounded-og-sm text-left outline-hidden",
450
+ "text-og-status-failed focus-visible:ring-2 focus-visible:ring-og-accent/45",
451
+ )}
452
+ >
453
+ <span className="og-transcription-error-label max-w-56 line-clamp-2 text-og-xs leading-tight max-sm:max-w-32">
409
454
  {errorMessage}
410
455
  </span>
411
- </Tip>
456
+ <XIcon className="size-3 shrink-0 opacity-70" aria-hidden />
457
+ </button>
412
458
  ) : null}
413
459
  <span
414
460
  className="sr-only"
@@ -424,6 +470,42 @@ export function ComposerTranscriptionControl({
424
470
  );
425
471
  }
426
472
 
473
+ /** Elapsed dictation time; warns before the automatic stop-and-transcribe. */
474
+ function RecordingClock({
475
+ startedAt,
476
+ maxSeconds,
477
+ remainingLabel,
478
+ }: {
479
+ startedAt: number;
480
+ maxSeconds: number | null;
481
+ remainingLabel: string;
482
+ }) {
483
+ const [now, setNow] = useState(() => Date.now());
484
+ useEffect(() => {
485
+ const timer = setInterval(() => setNow(Date.now()), 500);
486
+ return () => clearInterval(timer);
487
+ }, []);
488
+ const elapsed = Math.max(0, Math.floor((now - startedAt) / 1_000));
489
+ const remaining = maxSeconds === null ? null : Math.max(0, maxSeconds - elapsed);
490
+ const warn = remaining !== null && remaining <= 15;
491
+ const label = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, "0")}`;
492
+ return (
493
+ <span
494
+ data-recording-clock
495
+ title={warn ? remainingLabel.replace("{seconds}", String(remaining)) : undefined}
496
+ className={cn(
497
+ "min-w-[2.25rem] text-right text-og-xs tabular-nums",
498
+ warn ? "font-medium text-og-status-failed" : "text-og-fg-muted",
499
+ )}
500
+ >
501
+ {label}
502
+ {warn ? (
503
+ <span className="sr-only">{remainingLabel.replace("{seconds}", String(remaining))}</span>
504
+ ) : null}
505
+ </span>
506
+ );
507
+ }
508
+
427
509
  function VoiceWaveform({
428
510
  stream,
429
511
  mode,
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ComputerAction,
3
+ ComputerActionReceipt,
3
4
  ComputerClipboard,
4
5
  ComputerFrame,
5
6
  ComputerObservation,
@@ -42,7 +43,11 @@ import {
42
43
  type ComputerFrameWebSocketFactory,
43
44
  useComputerFrameStream,
44
45
  } from "../hooks/use-computer-frame-stream";
45
- import { useComputerSession } from "../hooks/use-computer-session";
46
+ import {
47
+ computerActionReceiptError,
48
+ isStaleComputerActionResult,
49
+ useComputerSession,
50
+ } from "../hooks/use-computer-session";
46
51
  import { useComputerInputPosture } from "../hooks/use-computer-input-posture";
47
52
  import { useComputerSessions } from "../hooks/use-computer-sessions";
48
53
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
@@ -174,6 +179,7 @@ export function ComputerViewer({
174
179
 
175
180
  const notifyError = useCallback(
176
181
  (cause: unknown, fallback: string) => {
182
+ if (isStaleComputerActionResult(cause)) return;
177
183
  onNotify?.({
178
184
  kind: "error",
179
185
  message: cause instanceof Error ? cause.message : fallback,
@@ -490,19 +496,24 @@ export function ComputerViewer({
490
496
  ]);
491
497
 
492
498
  const perform = useCallback(
493
- async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
499
+ async (
500
+ action: ComputerAction,
501
+ frame: ComputerFrame | null,
502
+ operationId?: string,
503
+ ): Promise<ComputerActionReceipt> => {
494
504
  if (computer.controlError) throw computer.controlError;
495
505
  if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
496
506
  let receipt;
497
507
  if (action.type === "pointer") {
498
508
  if (!frame) throw new Error("Desktop view is not ready for pointer input.");
499
- receipt = await actFromFrame(action, frame);
509
+ receipt = await actFromFrame(action, frame, operationId);
500
510
  } else {
501
- receipt = await act(action);
511
+ receipt = await act(action, operationId);
502
512
  }
503
- if (receipt.state !== "completed") {
504
- throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
513
+ if (!isStaleComputerActionResult(receipt) && receipt.state !== "completed") {
514
+ throw computerActionReceiptError(receipt);
505
515
  }
516
+ return receipt;
506
517
  },
507
518
  [act, actFromFrame, computer.controlError, inputDenied],
508
519
  );
@@ -519,14 +530,15 @@ export function ComputerViewer({
519
530
  return;
520
531
  event.preventDefault();
521
532
  event.stopPropagation();
522
- void perform({ type: "clipboard", operation: "copy" }, null)
523
- .then(() => computer.readClipboard())
524
- .then(async (clipboard) => {
525
- if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
526
- throw new Error("Desktop text could not be copied to the local clipboard");
527
- }
528
- })
529
- .catch((cause) => notifyError(cause, "Could not copy from the desktop."));
533
+ void (async () => {
534
+ const receipt = await perform({ type: "clipboard", operation: "copy" }, null);
535
+ if (isStaleComputerActionResult(receipt)) return;
536
+ const clipboard = await computer.readClipboard();
537
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
538
+ if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
539
+ throw new Error("Desktop text could not be copied to the local clipboard");
540
+ }
541
+ })().catch((cause) => notifyError(cause, "Could not copy from the desktop."));
530
542
  },
531
543
  [computer, notifyError, perform, rfbInputEnabled, rfbStream],
532
544
  );
@@ -543,8 +555,11 @@ export function ComputerViewer({
543
555
  // ownership is asynchronous, so a successful write response alone does
544
556
  // not prove that the graphical seat can already serve the selection.
545
557
  void (async () => {
546
- await perform({ type: "clipboard", operation: "write", text }, null);
547
- assertExactComputerClipboard(await computer.readClipboard(), text);
558
+ const receipt = await perform({ type: "clipboard", operation: "write", text }, null);
559
+ if (isStaleComputerActionResult(receipt)) return;
560
+ const clipboard = await computer.readClipboard();
561
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
562
+ assertExactComputerClipboard(clipboard, text);
548
563
  await perform({ type: "clipboard", operation: "paste" }, null);
549
564
  })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
550
565
  return true;
@@ -750,6 +765,10 @@ export function ComputerViewer({
750
765
  pointerInput={
751
766
  attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true
752
767
  }
768
+ pointerClickContinuation={
769
+ attachmentInputAllowed &&
770
+ computer.session?.capabilities?.pointerClickContinuation === true
771
+ }
753
772
  keyboardInput={
754
773
  attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true
755
774
  }
@@ -1194,6 +1213,12 @@ function ComputerLifecyclePanel(props: {
1194
1213
  );
1195
1214
  }
1196
1215
 
1216
+ type ComputerQueuedAction = {
1217
+ operationId: string;
1218
+ started: Promise<boolean>;
1219
+ completion: Promise<void>;
1220
+ };
1221
+
1197
1222
  function ComputerViewport(props: {
1198
1223
  frame: ComputerFrame | null;
1199
1224
  observation: ComputerObservation | null;
@@ -1208,8 +1233,13 @@ function ComputerViewport(props: {
1208
1233
  backgroundInput: boolean;
1209
1234
  clipboardEnabled: boolean;
1210
1235
  pointerInput: boolean;
1236
+ pointerClickContinuation: boolean;
1211
1237
  keyboardInput: boolean;
1212
- onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
1238
+ onAction: (
1239
+ action: ComputerAction,
1240
+ frame: ComputerFrame | null,
1241
+ operationId?: string,
1242
+ ) => Promise<ComputerActionReceipt>;
1213
1243
  onReadClipboard: () => Promise<ComputerClipboard>;
1214
1244
  onReconnect: () => void;
1215
1245
  onError: (cause: unknown) => void;
@@ -1224,6 +1254,11 @@ function ComputerViewport(props: {
1224
1254
  x: number;
1225
1255
  y: number;
1226
1256
  frame: ComputerFrame;
1257
+ clientX: number;
1258
+ clientY: number;
1259
+ canvasRect: Pick<DOMRect, "left" | "top" | "width" | "height">;
1260
+ pending: boolean;
1261
+ delivery: ComputerQueuedAction | null;
1227
1262
  } | null>(null);
1228
1263
  const wheelRef = useRef<{
1229
1264
  x: number;
@@ -1394,24 +1429,48 @@ function ComputerViewport(props: {
1394
1429
  action: ComputerAction,
1395
1430
  frame: ComputerFrame | null,
1396
1431
  after?: (isCurrent: () => boolean) => Promise<void>,
1432
+ continuation?: ComputerQueuedAction,
1397
1433
  ) => {
1398
1434
  const epoch = actionQueueEpochRef.current;
1399
1435
  const dispatch = actionRef.current;
1400
1436
  const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
1401
- actionTailRef.current = actionTailRef.current
1402
- .catch(() => undefined)
1403
- .then(async () => {
1404
- if (!isCurrent()) return;
1405
- await dispatch(action, frame);
1406
- if (!isCurrent()) return;
1407
- await after?.(isCurrent);
1437
+ const operationId = crypto.randomUUID();
1438
+ let markStarted!: (value: boolean) => void;
1439
+ const started = new Promise<boolean>((resolve) => {
1440
+ markStarted = resolve;
1441
+ });
1442
+ const previous = actionTailRef.current;
1443
+ // Only the real second click may start before its first HTTP receipt.
1444
+ // The controller queue and native causal proof still serialize delivery.
1445
+ const ready =
1446
+ continuation && previous === continuation.completion
1447
+ ? continuation.started
1448
+ : previous.then(() => true);
1449
+ const delivery = ready
1450
+ .then(async (firstStarted) => {
1451
+ if (!firstStarted || !isCurrent()) {
1452
+ markStarted(false);
1453
+ return;
1454
+ }
1455
+ const pending = dispatch(action, frame, operationId);
1456
+ markStarted(true);
1457
+ const receipt = await pending;
1458
+ const isCurrentResult = () => isCurrent() && !isStaleComputerActionResult(receipt);
1459
+ if (!isCurrentResult()) return;
1460
+ await after?.(isCurrentResult);
1408
1461
  })
1409
1462
  .catch((cause) => {
1410
- if (!isCurrent()) return;
1463
+ markStarted(false);
1464
+ if (!isCurrent() || isStaleComputerActionResult(cause)) return;
1411
1465
  actionQueueEpochRef.current += 1;
1412
1466
  clearBufferedInput();
1413
1467
  errorRef.current(cause);
1414
1468
  });
1469
+ const completion = continuation
1470
+ ? Promise.all([previous, delivery]).then(() => undefined)
1471
+ : delivery;
1472
+ actionTailRef.current = completion;
1473
+ return { operationId, started, completion };
1415
1474
  },
1416
1475
  [clearBufferedInput],
1417
1476
  );
@@ -1436,6 +1495,7 @@ function ComputerViewport(props: {
1436
1495
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1437
1496
  clickTimerRef.current = null;
1438
1497
  lastClickRef.current = null;
1498
+ if (!pending.pending) return;
1439
1499
  enqueue(
1440
1500
  {
1441
1501
  type: "pointer",
@@ -1469,7 +1529,13 @@ function ComputerViewport(props: {
1469
1529
 
1470
1530
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
1471
1531
  const frame = paintedFrameRef.current;
1472
- if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1532
+ if (
1533
+ !frame ||
1534
+ (!props.pointerClickContinuation && props.mutating) ||
1535
+ !rawInputEnabled ||
1536
+ event.button !== 0
1537
+ )
1538
+ return;
1473
1539
  // Preserve the keyboard sink's focus through the canvas pointer default action.
1474
1540
  event.preventDefault();
1475
1541
  if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
@@ -1490,6 +1556,7 @@ function ComputerViewport(props: {
1490
1556
  const start = pointerStartRef.current;
1491
1557
  pointerStartRef.current = null;
1492
1558
  if (!start || start.pointerId !== event.pointerId) return;
1559
+ const canvasRect = event.currentTarget.getBoundingClientRect();
1493
1560
  const from = point(start.frame, start.x, start.y);
1494
1561
  const to = point(start.frame, event.clientX, event.clientY);
1495
1562
  if (!from || !to) return;
@@ -1514,8 +1581,18 @@ function ComputerViewport(props: {
1514
1581
  if (
1515
1582
  previous &&
1516
1583
  now - previous.at < 280 &&
1517
- Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
1518
- sameFrameFence(previous.frame, start.frame)
1584
+ previous.pending === !props.pointerClickContinuation &&
1585
+ (props.pointerClickContinuation
1586
+ ? Math.hypot(previous.clientX - event.clientX, previous.clientY - event.clientY) < 6 &&
1587
+ previous.canvasRect.left === canvasRect.left &&
1588
+ previous.canvasRect.top === canvasRect.top &&
1589
+ previous.canvasRect.width === canvasRect.width &&
1590
+ previous.canvasRect.height === canvasRect.height &&
1591
+ sameComputerTarget(previous.frame, start.frame) &&
1592
+ previous.frame.width === start.frame.width &&
1593
+ previous.frame.height === start.frame.height
1594
+ : Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
1595
+ sameFrameFence(previous.frame, start.frame))
1519
1596
  ) {
1520
1597
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1521
1598
  clickTimerRef.current = null;
@@ -1524,15 +1601,47 @@ function ComputerViewport(props: {
1524
1601
  {
1525
1602
  type: "pointer",
1526
1603
  frameId: start.frame.frameId,
1527
- action: "double_click",
1604
+ action: props.pointerClickContinuation ? "click" : "double_click",
1605
+ ...(props.pointerClickContinuation ? { clickCount: 2 as const } : {}),
1606
+ ...(previous.delivery
1607
+ ? { continuationOfOperationId: previous.delivery.operationId }
1608
+ : {}),
1609
+ x: to.x,
1610
+ y: to.y,
1611
+ },
1612
+ start.frame,
1613
+ undefined,
1614
+ previous.delivery ?? undefined,
1615
+ );
1616
+ return;
1617
+ }
1618
+ flushPendingClick();
1619
+ lastClickRef.current = {
1620
+ at: now,
1621
+ x: to.x,
1622
+ y: to.y,
1623
+ frame: start.frame,
1624
+ clientX: event.clientX,
1625
+ clientY: event.clientY,
1626
+ canvasRect,
1627
+ pending: !props.pointerClickContinuation,
1628
+ delivery: null,
1629
+ };
1630
+ if (props.pointerClickContinuation) {
1631
+ const delivery = enqueue(
1632
+ {
1633
+ type: "pointer",
1634
+ frameId: start.frame.frameId,
1635
+ action: "click",
1636
+ clickCount: 1,
1528
1637
  x: to.x,
1529
1638
  y: to.y,
1530
1639
  },
1531
1640
  start.frame,
1532
1641
  );
1642
+ if (lastClickRef.current) lastClickRef.current.delivery = delivery;
1533
1643
  return;
1534
1644
  }
1535
- lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
1536
1645
  clickTimerRef.current = setTimeout(() => {
1537
1646
  clickTimerRef.current = null;
1538
1647
  lastClickRef.current = null;
@@ -230,7 +230,7 @@ const baseComponents: Components = {
230
230
  ),
231
231
  td: ({ children, ...props }) => (
232
232
  <td
233
- className="border-b border-og-border/70 px-0 py-1.5 pr-4 align-top text-og-fg-muted [tr:last-child>&]:border-b-0"
233
+ className="border-b border-og-border/70 px-0 py-1.5 pr-4 align-top text-og-fg-muted"
234
234
  {...props}
235
235
  >
236
236
  {children}
@@ -615,7 +615,12 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
615
615
  <div className="overflow-x-auto" tabIndex={0}>
616
616
  <table
617
617
  ref={tableRef}
618
- className={cn("w-full min-w-0 border-collapse text-og-base", className)}
618
+ className={cn(
619
+ // Leading `&` only: a trailing one (`tr:last-child>&`) under the scoped
620
+ // selector list needs `:is()`, which bundlers warn about for older targets.
621
+ "w-full min-w-0 border-collapse text-og-base [&_tr:last-child>td]:border-b-0",
622
+ className,
623
+ )}
619
624
  {...props}
620
625
  >
621
626
  {children}
@@ -2496,6 +2496,16 @@ export function MessageTimeline({
2496
2496
  }
2497
2497
  releasePinAfterScrollSettled(node);
2498
2498
  };
2499
+ // React 18 has no `onScrollEnd` prop: it warns and never calls it, while
2500
+ // `supportsScrollEndEvent()` still makes the camera wait for scrollend. A
2501
+ // native listener (re-bound each commit, so it always sees this render's
2502
+ // handler and scroller) works on every supported React version.
2503
+ useEffect(() => {
2504
+ const node = scrollRef.current;
2505
+ if (!node) return;
2506
+ node.addEventListener("scrollend", onScrollEnd);
2507
+ return () => node.removeEventListener("scrollend", onScrollEnd);
2508
+ });
2499
2509
 
2500
2510
  const timeline = (
2501
2511
  <AllowancePresentationProvider
@@ -2538,7 +2548,6 @@ export function MessageTimeline({
2538
2548
  }
2539
2549
  tabIndex={-1}
2540
2550
  onScroll={onScroll}
2541
- onScrollEnd={onScrollEnd}
2542
2551
  onWheel={onWheel}
2543
2552
  onTouchStart={(event) => {
2544
2553
  stopSettlement();
@@ -34,6 +34,7 @@ import {
34
34
  createWorkspaceRetainedArtifactLoader,
35
35
  createWorkspaceRetainedVideoLoader,
36
36
  } from "../timeline/retained-loaders";
37
+ import { useRealtimeVoiceModels } from "../hooks/use-realtime-voice-models";
37
38
  import { projectPendingApprovals } from "../approvals";
38
39
  import { ApprovalSurface } from "./approval-surface";
39
40
  import { ChatComposer, type ChatComposerProps } from "./chat-composer";
@@ -126,6 +127,12 @@ export type SessionConversationProps = ClientOverride &
126
127
  * (`createSessionProxyHandler({ voiceInput: false })` turns it off).
127
128
  */
128
129
  voiceInput?: boolean | undefined;
130
+ /**
131
+ * Live speech-to-speech voice in the composer. Defaults to true; the voice
132
+ * button appears only when the workspace offers an available voice model
133
+ * and the proxy has not turned it off (`realtimeVoice: false`).
134
+ */
135
+ realtimeVoice?: boolean | undefined;
129
136
  /**
130
137
  * Show the model/reasoning picker. End users of an embedded product rarely
131
138
  * choose models, so it is hidden unless this is `true` or the client config
@@ -210,6 +217,7 @@ function Conversation({
210
217
  allowanceExhaustedLabels,
211
218
  attachments: attachmentsRequested = true,
212
219
  voiceInput: voiceInputRequested = true,
220
+ realtimeVoice: realtimeVoiceRequested = true,
213
221
  modelPicker,
214
222
  modelPickerProps,
215
223
  userMessageDisclosureLabels,
@@ -319,6 +327,30 @@ function Conversation({
319
327
  if (feed.error) void feed.jumpToLatest();
320
328
  };
321
329
  const running = status === "running" || status === "recovering" || status === "waiting_capacity";
330
+ const voiceModels = useRealtimeVoiceModels(
331
+ context.client,
332
+ context.workspaceId,
333
+ realtimeVoiceRequested && config.realtimeVoice && !importedArchive,
334
+ );
335
+ const [voiceActive, setVoiceActive] = useState(false);
336
+ const voiceControl =
337
+ composer.effectiveControl ?? queue.effectiveControl ?? detail.session?.effectiveControl;
338
+ const voice =
339
+ voiceModels.length > 0 && status && !terminal && voiceControl ? (
340
+ <Suspense fallback={null}>
341
+ <LazyEmbeddedRealtimeVoice
342
+ client={context.client}
343
+ workspaceId={context.workspaceId}
344
+ models={voiceModels}
345
+ sessionId={sessionId}
346
+ sessionStatus={status}
347
+ effectiveControl={voiceControl}
348
+ events={feed.events}
349
+ eventsReady={!feed.initialLoading}
350
+ onVoiceActiveChange={setVoiceActive}
351
+ />
352
+ </Suspense>
353
+ ) : null;
322
354
  return (
323
355
  <div
324
356
  className={cn(
@@ -467,6 +499,17 @@ function Conversation({
467
499
  {...(transcription && composerProps?.transcription === undefined
468
500
  ? { transcription }
469
501
  : {})}
502
+ actionsStart={
503
+ voice ? (
504
+ <>
505
+ {composerProps?.actionsStart}
506
+ {voice}
507
+ </>
508
+ ) : (
509
+ composerProps?.actionsStart
510
+ )
511
+ }
512
+ transcriptionSuppressed={composerProps?.transcriptionSuppressed ?? voiceActive}
470
513
  composer={composer}
471
514
  attachments={uploadsEnabled ? files : undefined}
472
515
  disabled={terminal || composerProps?.disabled}
@@ -534,6 +577,7 @@ function GoalChrome({ sessionId, client, workspaceId, events, chrome }: Conversa
534
577
  });
535
578
  return <SessionChrome {...chrome} goal={goal} />;
536
579
  }
580
+ const LazyEmbeddedRealtimeVoice = lazy(() => import("../realtime/embedded-voice"));
537
581
 
538
582
  const LazyChatInteractiveBlock = lazy(() =>
539
583
  import("./artifacts/chat-interactive-block").then((module) => ({