@runtypelabs/persona 4.25.0 → 4.26.1

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 (236) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
  4. package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +14 -0
  8. package/dist/animations-extra.d.ts +14 -0
  9. package/dist/approval-ui-QAKWNFNM.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +186 -8
  12. package/dist/approval-ui.d.ts +186 -8
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
  15. package/dist/artifacts-ui.cjs +4 -4
  16. package/dist/artifacts-ui.d.cts +186 -8
  17. package/dist/artifacts-ui.d.ts +186 -8
  18. package/dist/artifacts-ui.js +5 -5
  19. package/dist/chunk-2ANTRKTV.js +1 -0
  20. package/dist/chunk-4HPTPUC6.js +1 -0
  21. package/dist/chunk-6V3MNEKB.js +1 -0
  22. package/dist/chunk-GKPCIQPB.js +1 -0
  23. package/dist/chunk-HGUCGM3K.js +9 -0
  24. package/dist/chunk-IO5VVUKP.js +3 -0
  25. package/dist/chunk-J4J3Y2P2.js +1 -0
  26. package/dist/chunk-K6HKIP5K.js +1 -0
  27. package/dist/chunk-K7NA7G7J.js +1 -0
  28. package/dist/chunk-MXUTS3LB.js +1 -0
  29. package/dist/chunk-PDPVORUF.js +1 -0
  30. package/dist/chunk-QRKGQPU7.js +1 -0
  31. package/dist/chunk-S6PBWE75.js +1 -0
  32. package/dist/chunk-T7QM7YMF.js +1 -0
  33. package/dist/chunk-TR2H475E.js +1 -0
  34. package/dist/chunk-UPO4GUFC.js +1 -0
  35. package/dist/chunk-ZC66NAIR.js +1 -0
  36. package/dist/client-history-GOZWBDA5.js +1 -0
  37. package/dist/client-history.js +1 -0
  38. package/dist/client-stream-42FOI3EC.js +4 -0
  39. package/dist/client-stream.js +5 -0
  40. package/dist/codegen.cjs +1 -1
  41. package/dist/codegen.js +1 -1
  42. package/dist/context-mentions-E7HJYWO6.js +161 -0
  43. package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
  44. package/dist/context-mentions.cjs +7 -7
  45. package/dist/context-mentions.d.cts +186 -8
  46. package/dist/context-mentions.d.ts +186 -8
  47. package/dist/context-mentions.js +8 -8
  48. package/dist/event-stream-view-FOIJHFQO.js +1 -0
  49. package/dist/event-stream-view.cjs +1 -1
  50. package/dist/event-stream-view.d.cts +189 -31
  51. package/dist/event-stream-view.d.ts +189 -31
  52. package/dist/event-stream-view.js +1 -1
  53. package/dist/forms-ui.d.cts +227 -667
  54. package/dist/forms-ui.d.ts +227 -667
  55. package/dist/history-shell-QDQ44PSE.js +3 -0
  56. package/dist/history-shell.js +3 -0
  57. package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
  58. package/dist/history-view.cjs +2 -2
  59. package/dist/history-view.d.cts +35 -1
  60. package/dist/history-view.d.ts +35 -1
  61. package/dist/history-view.js +2 -2
  62. package/dist/index.cjs +80 -74
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +233 -738
  65. package/dist/index.d.ts +233 -738
  66. package/dist/index.global.js +3 -215
  67. package/dist/index.global.js.map +1 -1
  68. package/dist/index.js +81 -75
  69. package/dist/index.js.map +1 -1
  70. package/dist/install.global.js +1 -1
  71. package/dist/install.global.js.map +1 -1
  72. package/dist/launcher.global.js +3 -67
  73. package/dist/launcher.global.js.map +1 -1
  74. package/dist/markdown-parsers.js +22 -22
  75. package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
  76. package/dist/runtype-tts.js +1 -1
  77. package/dist/session-actions-ISSKSI66.js +1 -0
  78. package/dist/session-actions.js +1 -0
  79. package/dist/session-reconnect.d.cts +186 -8
  80. package/dist/session-reconnect.d.ts +186 -8
  81. package/dist/smart-dom-reader.cjs +13 -13
  82. package/dist/smart-dom-reader.d.cts +186 -8
  83. package/dist/smart-dom-reader.d.ts +186 -8
  84. package/dist/smart-dom-reader.js +11 -11
  85. package/dist/theme-editor-preview.cjs +116 -98
  86. package/dist/theme-editor-preview.d.cts +188 -8
  87. package/dist/theme-editor-preview.d.ts +188 -8
  88. package/dist/theme-editor-preview.js +49 -54
  89. package/dist/theme-editor.cjs +2 -2
  90. package/dist/theme-editor.d.cts +187 -23
  91. package/dist/theme-editor.d.ts +187 -23
  92. package/dist/theme-editor.js +2 -2
  93. package/dist/theme-reference.d.cts +14 -0
  94. package/dist/theme-reference.d.ts +14 -0
  95. package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
  96. package/dist/ui-extras.js +72 -0
  97. package/dist/voice-runtime-XLU6YK2N.js +17 -0
  98. package/dist/voice-runtime.cjs +17 -1
  99. package/dist/voice-runtime.d.cts +11656 -232
  100. package/dist/voice-runtime.d.ts +11656 -232
  101. package/dist/voice-runtime.js +17 -1
  102. package/dist/voice-worklet-player.cjs +19 -4
  103. package/dist/voice-worklet-player.d.cts +9 -10
  104. package/dist/voice-worklet-player.d.ts +9 -10
  105. package/dist/voice-worklet-player.js +19 -4
  106. package/dist/webmcp-polyfill.js +1 -1
  107. package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
  108. package/dist/webmcp-runtime.cjs +2 -2
  109. package/dist/webmcp-runtime.d.cts +1 -33
  110. package/dist/webmcp-runtime.d.ts +1 -33
  111. package/dist/webmcp-runtime.js +2 -2
  112. package/dist/widget.css +1 -1
  113. package/package.json +11 -5
  114. package/src/approval-paused-tool-settle.test.ts +325 -0
  115. package/src/client-approve.test.ts +311 -0
  116. package/src/client-history-eager.ts +9 -0
  117. package/src/client-history-loader.ts +23 -0
  118. package/src/client-history.ts +579 -0
  119. package/src/client-resume.ts +177 -0
  120. package/src/client-stream-eager.ts +12 -0
  121. package/src/client-stream-loader.ts +25 -0
  122. package/src/client-stream.ts +1880 -0
  123. package/src/client.test.ts +31 -1
  124. package/src/client.ts +293 -2607
  125. package/src/components/approval-actions.test.ts +27 -0
  126. package/src/components/approval-actions.ts +27 -3
  127. package/src/components/message-bubble.test.ts +19 -0
  128. package/src/components/message-bubble.ts +2 -0
  129. package/src/components/registry.ts +6 -6
  130. package/src/components/tool-bubble.test.ts +9 -0
  131. package/src/components/tool-bubble.ts +1 -0
  132. package/src/history-shell-bundle.test.ts +54 -0
  133. package/src/history-shell-eager.ts +12 -0
  134. package/src/history-shell-loader.ts +27 -0
  135. package/src/history-shell.ts +2362 -0
  136. package/src/history-view-loader.ts +2 -0
  137. package/src/history-view.ts +3 -0
  138. package/src/index-core.ts +6 -0
  139. package/src/index-global.ts +90 -248
  140. package/src/index.ts +13 -0
  141. package/src/install.ts +6 -4
  142. package/src/internal/runtype-history-provider.test.ts +1 -0
  143. package/src/internal/runtype-history-provider.ts +28 -13
  144. package/src/plugins/registry.ts +8 -8
  145. package/src/session-actions-chunk.test.ts +55 -0
  146. package/src/session-actions-eager.ts +11 -0
  147. package/src/session-actions-loader.ts +37 -0
  148. package/src/session-actions.ts +909 -0
  149. package/src/session-history.test.ts +3 -1
  150. package/src/session.ts +895 -1538
  151. package/src/session.voice.test.ts +1171 -5
  152. package/src/styles/widget.css +58 -0
  153. package/src/theme-editor/state.ts +91 -91
  154. package/src/types.ts +204 -5
  155. package/src/ui-ask-user-sheet.ts +382 -0
  156. package/src/ui-extras-eager.ts +10 -0
  157. package/src/ui-extras-entry.ts +9 -0
  158. package/src/ui-extras-loader.ts +26 -0
  159. package/src/ui.artifact-rail-resize.test.ts +2 -1
  160. package/src/ui.ask-user-chunk.test.ts +93 -0
  161. package/src/ui.history-chunk.test.ts +2 -1
  162. package/src/ui.history-config-provider.test.ts +2 -1
  163. package/src/ui.history-confirm-fallback.test.ts +142 -0
  164. package/src/ui.history-controller.test.ts +2 -1
  165. package/src/ui.history-lifecycle.test.ts +2 -1
  166. package/src/ui.history-render-hooks.test.ts +2 -1
  167. package/src/ui.history-shell.test.ts +2 -1
  168. package/src/ui.rail-overlay.test.ts +2 -1
  169. package/src/ui.rail-resize.test.ts +2 -1
  170. package/src/ui.rail-trigger-pending.test.ts +2 -1
  171. package/src/ui.ts +340 -2770
  172. package/src/utils/attachment-manager.ts +67 -67
  173. package/src/utils/constants.ts +3 -0
  174. package/src/utils/context-mention-controller.ts +323 -323
  175. package/src/utils/context-mention-manager.ts +40 -40
  176. package/src/utils/context-mention-orchestrator.ts +28 -0
  177. package/src/utils/event-stream-buffer.ts +50 -50
  178. package/src/utils/event-stream-store.ts +43 -43
  179. package/src/utils/throughput-tracker.ts +34 -34
  180. package/src/utils/virtual-scroller.ts +67 -67
  181. package/src/utils/visitor-store.test.ts +50 -0
  182. package/src/utils/visitor-store.ts +23 -5
  183. package/src/voice/__fixtures__/voice-wire/README.md +1 -0
  184. package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
  185. package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
  186. package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
  187. package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
  188. package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
  189. package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
  190. package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
  191. package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
  192. package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
  193. package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
  194. package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
  195. package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
  196. package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
  197. package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
  198. package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
  199. package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
  200. package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
  201. package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
  202. package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
  203. package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
  204. package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
  205. package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
  206. package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
  207. package/src/voice/audio-playback-manager.test.ts +57 -0
  208. package/src/voice/audio-playback-manager.ts +120 -98
  209. package/src/voice/browser-voice-provider.ts +37 -37
  210. package/src/voice/fallback-speech-engine.ts +22 -16
  211. package/src/voice/keyed-voice-transcript.ts +314 -0
  212. package/src/voice/mic-state-styles.ts +182 -0
  213. package/src/voice/read-aloud-controller.ts +46 -44
  214. package/src/voice/runtype-speech-engine.ts +35 -33
  215. package/src/voice/runtype-voice-provider.ts +680 -189
  216. package/src/voice/session-voice-wiring.ts +266 -0
  217. package/src/voice/voice-delegation.test.ts +71 -0
  218. package/src/voice/voice-delegation.ts +364 -0
  219. package/src/voice/voice-turn-tracker.ts +6 -6
  220. package/src/voice/voice.test.ts +1060 -8
  221. package/src/voice/worklet-playback-engine.test.ts +81 -0
  222. package/src/voice/worklet-playback-engine.ts +21 -3
  223. package/src/voice-runtime-loader.ts +4 -1
  224. package/src/voice-runtime.ts +7 -0
  225. package/src/webmcp-runtime-entry.ts +41 -39
  226. package/dist/approval-ui-3NMYNXBD.js +0 -1
  227. package/dist/chunk-5EIIHQLQ.js +0 -1
  228. package/dist/chunk-7CO4T5KB.js +0 -1
  229. package/dist/chunk-EFJBUH5F.js +0 -1
  230. package/dist/chunk-R2TMIMDI.js +0 -3
  231. package/dist/chunk-UTVLOTWB.js +0 -9
  232. package/dist/context-mentions-45MODWOY.js +0 -161
  233. package/dist/event-stream-view-GL3ADRY2.js +0 -1
  234. package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
  235. package/dist/voice-runtime-7DA7MJH4.js +0 -1
  236. package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.25.0",
3
+ "version": "4.26.1",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -149,6 +149,7 @@
149
149
  "fake-indexeddb": "^6.2.5",
150
150
  "rimraf": "^5.0.5",
151
151
  "size-limit": "^12.1.0",
152
+ "terser": "^5.49.0",
152
153
  "tsup": "^8.0.1",
153
154
  "typescript": "^5.4.5",
154
155
  "vitest": "^4.0.9"
@@ -180,10 +181,15 @@
180
181
  "access": "public"
181
182
  },
182
183
  "scripts": {
183
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:context-mentions && pnpm run build:context-mentions-inline && pnpm run build:history-view && pnpm run build:event-stream-view && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations && pnpm run build:animations-extra && pnpm run build:approval-ui && pnpm run build:session-reconnect && pnpm run build:webmcp-runtime && pnpm run build:icons-extra && pnpm run build:artifacts-ui && pnpm run build:voice-runtime && pnpm run build:forms-ui && pnpm run check:dist-vite8 && pnpm run check:size",
184
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client-stream && pnpm run build:client-history && pnpm run build:ui-extras && pnpm run build:session-actions && pnpm run build:history-shell && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:context-mentions && pnpm run build:context-mentions-inline && pnpm run build:history-view && pnpm run build:event-stream-view && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations && pnpm run build:animations-extra && pnpm run build:approval-ui && pnpm run build:session-reconnect && pnpm run build:webmcp-runtime && pnpm run build:icons-extra && pnpm run build:artifacts-ui && pnpm run build:voice-runtime && pnpm run build:forms-ui && pnpm run check:dist-vite8 && pnpm run check:size",
184
185
  "check:size": "node scripts/check-size.mjs",
185
186
  "check:dist-vite8": "node scripts/check-dist-no-in-for-init.mjs",
186
187
  "build:markdown-parsers": "tsup --config tsup.markdown-parsers.config.ts",
188
+ "build:client-stream": "tsup --config tsup.client-stream.config.ts",
189
+ "build:client-history": "tsup --config tsup.client-history.config.ts",
190
+ "build:ui-extras": "tsup --config tsup.ui-extras.config.ts",
191
+ "build:session-actions": "tsup --config tsup.session-actions.config.ts",
192
+ "build:history-shell": "tsup --config tsup.history-shell.config.ts",
187
193
  "build:plugin-kit": "tsup src/plugin-kit.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
188
194
  "build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
189
195
  "build:theme-editor-preview": "tsup src/theme-editor-preview.ts --format esm,cjs --minify --dts --out-dir dist",
@@ -202,14 +208,14 @@
202
208
  "build:theme-ref": "tsup src/theme-reference.ts --format esm,cjs --minify --dts",
203
209
  "build:codegen": "tsup src/codegen.ts --format esm,cjs --minify --dts",
204
210
  "build:styles": "node scripts/build-styles.mjs",
205
- "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" --external @runtypelabs/persona/context-mentions --external @runtypelabs/persona/context-mentions-inline --external @runtypelabs/persona/history-view --external @runtypelabs/persona/event-stream-view --external @runtypelabs/persona/animations-extra --external @runtypelabs/persona/approval-ui --external @runtypelabs/persona/session-reconnect --external @runtypelabs/persona/webmcp-runtime --external @runtypelabs/persona/icons-extra --external @runtypelabs/persona/artifacts-ui --external @runtypelabs/persona/voice-runtime --external @runtypelabs/persona/forms-ui && tsup --config tsup.global.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
211
+ "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" --external @runtypelabs/persona/context-mentions --external @runtypelabs/persona/context-mentions-inline --external @runtypelabs/persona/history-view --external @runtypelabs/persona/event-stream-view --external @runtypelabs/persona/animations-extra --external @runtypelabs/persona/approval-ui --external @runtypelabs/persona/session-reconnect --external @runtypelabs/persona/webmcp-runtime --external @runtypelabs/persona/icons-extra --external @runtypelabs/persona/artifacts-ui --external @runtypelabs/persona/voice-runtime --external @runtypelabs/persona/forms-ui && tsup --config tsup.global.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\" && node scripts/terser-global.mjs dist/index.global.js",
206
212
  "build:webmcp-polyfill": "tsup --config tsup.webmcp-polyfill.config.ts",
207
213
  "build:context-mentions": "tsup --config tsup.context-mentions.config.ts",
208
214
  "build:context-mentions-inline": "tsup --config tsup.context-mentions-inline.config.ts",
209
215
  "build:history-view": "tsup --config tsup.history-view.config.ts",
210
216
  "build:runtype-tts": "tsup --config tsup.runtype-tts.config.ts",
211
- "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
212
- "build:launcher": "tsup --config tsup.launcher.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/launcher-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/launcher'+ext);}\"",
217
+ "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting && node scripts/terser-global.mjs dist/install.global.js",
218
+ "build:launcher": "tsup --config tsup.launcher.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/launcher-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/launcher'+ext);}\" && node scripts/terser-global.mjs dist/launcher.global.js",
213
219
  "lint": "eslint . --ext .ts",
214
220
  "typecheck": "pnpm run check:runtype-types && tsc --noEmit",
215
221
  "test": "vitest",
@@ -0,0 +1,325 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { AgentWidgetClient } from "./client";
3
+ import { AgentWidgetSession } from "./session";
4
+ import type { AgentWidgetMessage } from "./types";
5
+
6
+ // Older servers resume an approval by re-running the agent, which re-issues the
7
+ // gated call under a NEW toolCallId, so no terminal frame arrives for the paused
8
+ // one; the session settles it when the stream ends. Current servers run the
9
+ // paused call itself under its original id, and that real result wins.
10
+
11
+ const originalFetch = global.fetch;
12
+ afterEach(() => {
13
+ global.fetch = originalFetch;
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ const sse = (events: Array<Record<string, unknown>>) =>
18
+ new Response(events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join(""), {
19
+ headers: { "Content-Type": "text/event-stream" },
20
+ });
21
+
22
+ const liveSession = (client: AgentWidgetClient) => {
23
+ (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
24
+ sessionId: "cs_123",
25
+ expiresAt: new Date(Date.now() + 60_000),
26
+ };
27
+ };
28
+
29
+ const pausedTool = (status: "running" | "complete" = "running"): AgentWidgetMessage => ({
30
+ id: "tool-toolu_paused",
31
+ role: "assistant",
32
+ content: "",
33
+ createdAt: new Date(Date.now() - 2_000).toISOString(),
34
+ streaming: status === "running",
35
+ variant: "tool",
36
+ toolCall: {
37
+ id: "toolu_paused",
38
+ name: "place_pickup_order",
39
+ status,
40
+ args: { item: "latte" },
41
+ startedAt: Date.now() - 2_000,
42
+ },
43
+ });
44
+
45
+ const pendingApproval = (toolCallId?: string): AgentWidgetMessage => ({
46
+ id: "approval-appr_1",
47
+ role: "assistant",
48
+ content: "",
49
+ createdAt: new Date(Date.now() - 1_000).toISOString(),
50
+ variant: "approval",
51
+ approval: {
52
+ id: "appr_1",
53
+ status: "pending",
54
+ agentId: "agent_abc",
55
+ executionId: "exec_abc",
56
+ toolName: "place_pickup_order",
57
+ description: "Place the order",
58
+ ...(toolCallId ? { toolCallId } : {}),
59
+ },
60
+ });
61
+
62
+ const setup = (initialMessages: AgentWidgetMessage[]) => {
63
+ let messages: AgentWidgetMessage[] = [];
64
+ const toolStates: Array<AgentWidgetMessage["toolCall"]> = [];
65
+ const session = new AgentWidgetSession(
66
+ { clientToken: "ct_live_demo", apiUrl: "https://api.runtype.com", initialMessages },
67
+ {
68
+ onMessagesChanged: (m) => {
69
+ messages = m;
70
+ toolStates.push(m.find((x) => x.toolCall?.id === "toolu_paused")?.toolCall);
71
+ },
72
+ onStatusChanged: () => {},
73
+ onStreamingChanged: () => {},
74
+ onError: () => {},
75
+ }
76
+ );
77
+ liveSession((session as unknown as { client: AgentWidgetClient }).client);
78
+ const approval = initialMessages.find((m) => m.variant === "approval")!.approval!;
79
+ return {
80
+ resolve: (decision: "approved" | "denied") => session.resolveApproval(approval, decision),
81
+ tool: (id = "toolu_paused") => messages.find((m) => m.toolCall?.id === id),
82
+ toolStates,
83
+ };
84
+ };
85
+
86
+ const resumeStream = (decision: "approved" | "denied" | "timeout", rerun = true) =>
87
+ sse([
88
+ { type: "approval_complete", executionId: "exec_abc", approvalId: "appr_1", decision },
89
+ ...(rerun
90
+ ? [
91
+ { type: "tool_start", toolCallId: "toolu_rerun", toolName: "place_pickup_order", toolType: "custom" },
92
+ {
93
+ type: "tool_complete",
94
+ toolCallId: "toolu_rerun",
95
+ toolName: "place_pickup_order",
96
+ success: decision === "approved",
97
+ result: decision === "approved" ? { orderId: "ord_1" } : undefined,
98
+ },
99
+ ]
100
+ : []),
101
+ { type: "execution_complete", kind: "agent", success: true },
102
+ ]);
103
+
104
+ describe("approval_start records the paused toolCallId", () => {
105
+ it("carries toolCallId onto the approval message", async () => {
106
+ const client = new AgentWidgetClient({ clientToken: "ct_live_demo", apiUrl: "https://api.runtype.com" });
107
+ const emitted: AgentWidgetMessage[] = [];
108
+ await client.processStream(
109
+ sse([
110
+ {
111
+ type: "approval_start",
112
+ executionId: "exec_abc",
113
+ approvalId: "appr_1",
114
+ toolCallId: "toolu_paused",
115
+ toolName: "place_pickup_order",
116
+ parameters: { item: "latte" },
117
+ },
118
+ ]).body!,
119
+ (event) => {
120
+ if (event.type === "message") emitted.push(event.message);
121
+ }
122
+ );
123
+ const approvalMessage = emitted.find((m) => m.variant === "approval");
124
+ expect(approvalMessage?.approval?.toolCallId).toBe("toolu_paused");
125
+ });
126
+ });
127
+
128
+ describe("AgentWidgetSession settles the approval-paused tool bubble", () => {
129
+ it("marks an approved call superseded and leaves the result on the re-run", async () => {
130
+ global.fetch = vi.fn().mockResolvedValue(resumeStream("approved"));
131
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
132
+ await resolve("approved");
133
+
134
+ expect(tool()?.toolCall).toMatchObject({
135
+ status: "complete",
136
+ approvalStatus: "approved",
137
+ superseded: true,
138
+ });
139
+ expect(tool()?.toolCall?.success).toBeUndefined();
140
+ expect(tool()?.streaming).toBe(false);
141
+ expect(tool("toolu_rerun")?.toolCall).toMatchObject({
142
+ status: "complete",
143
+ result: { orderId: "ord_1" },
144
+ });
145
+ });
146
+
147
+ it("settles a denied call as failed with a Denied error", async () => {
148
+ global.fetch = vi.fn().mockResolvedValue(resumeStream("denied", false));
149
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
150
+ await resolve("denied");
151
+
152
+ expect(tool()?.toolCall).toMatchObject({
153
+ status: "complete",
154
+ approvalStatus: "denied",
155
+ success: false,
156
+ error: "Denied",
157
+ });
158
+ expect(tool()?.toolCall?.superseded).toBeUndefined();
159
+ });
160
+
161
+ it("settles a timed-out call as failed", async () => {
162
+ global.fetch = vi.fn().mockResolvedValue(resumeStream("timeout", false));
163
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
164
+ await resolve("denied");
165
+
166
+ expect(tool()?.toolCall).toMatchObject({
167
+ status: "complete",
168
+ approvalStatus: "timeout",
169
+ success: false,
170
+ error: "Approval timed out",
171
+ });
172
+ });
173
+
174
+ it("leaves the bubble running when the server refuses the decision", async () => {
175
+ global.fetch = vi.fn().mockResolvedValue(Response.json({ error: "Failed to process approval" }, { status: 500 }));
176
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
177
+ await resolve("approved").catch(() => {});
178
+
179
+ expect(tool()?.toolCall?.status).toBe("running");
180
+ expect(tool()?.toolCall?.approvalStatus).toBeUndefined();
181
+ });
182
+
183
+ it("settles the call as timed out when the server reports the pause gone", async () => {
184
+ global.fetch = vi.fn().mockResolvedValue(
185
+ Response.json({ code: "APPROVAL_ALREADY_RESOLVED", message: "Already resolved" }, { status: 409 })
186
+ );
187
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
188
+ await resolve("approved").catch(() => {});
189
+
190
+ expect(tool()?.toolCall).toMatchObject({
191
+ status: "complete",
192
+ approvalStatus: "timeout",
193
+ success: false,
194
+ error: "Approval timed out",
195
+ });
196
+ });
197
+
198
+ it("lets the server's own result for the paused id complete an approved call", async () => {
199
+ global.fetch = vi.fn().mockResolvedValue(
200
+ sse([
201
+ { type: "approval_complete", executionId: "exec_abc", approvalId: "appr_1", decision: "approved" },
202
+ { type: "tool_start", toolCallId: "toolu_paused", toolName: "place_pickup_order", toolType: "custom" },
203
+ {
204
+ type: "tool_complete",
205
+ toolCallId: "toolu_paused",
206
+ toolName: "place_pickup_order",
207
+ success: true,
208
+ result: { orderId: "ord_1" },
209
+ },
210
+ { type: "execution_complete", kind: "agent", success: true },
211
+ ])
212
+ );
213
+ const { resolve, tool, toolStates } = setup([pausedTool(), pendingApproval("toolu_paused")]);
214
+ await resolve("approved");
215
+
216
+ expect(tool()?.toolCall).toMatchObject({ status: "complete", result: { orderId: "ord_1" } });
217
+ expect(toolStates.some((state) => state?.superseded)).toBe(false);
218
+ });
219
+
220
+ it("lets the server's own failed result for the paused id complete a denied call", async () => {
221
+ global.fetch = vi.fn().mockResolvedValue(
222
+ sse([
223
+ { type: "approval_complete", executionId: "exec_abc", approvalId: "appr_1", decision: "denied" },
224
+ { type: "tool_start", toolCallId: "toolu_paused", toolName: "place_pickup_order", toolType: "custom" },
225
+ {
226
+ type: "tool_complete",
227
+ toolCallId: "toolu_paused",
228
+ toolName: "place_pickup_order",
229
+ success: false,
230
+ error: 'Tool "place_pickup_order" was denied by the user.',
231
+ },
232
+ { type: "execution_complete", kind: "agent", success: true },
233
+ ])
234
+ );
235
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
236
+ await resolve("denied");
237
+
238
+ expect(tool()?.toolCall).toMatchObject({ status: "complete", success: false });
239
+ expect(tool()?.toolCall?.error).toContain("denied by the user");
240
+ });
241
+
242
+ it("does not touch a tool call that already completed", async () => {
243
+ global.fetch = vi.fn().mockResolvedValue(resumeStream("denied", false));
244
+ const { resolve, tool } = setup([pausedTool("complete"), pendingApproval("toolu_paused")]);
245
+ await resolve("denied");
246
+
247
+ expect(tool()?.toolCall?.success).toBeUndefined();
248
+ expect(tool()?.toolCall?.approvalStatus).toBeUndefined();
249
+ });
250
+
251
+ it("does nothing for an approval without a toolCallId", async () => {
252
+ global.fetch = vi.fn().mockResolvedValue(resumeStream("denied", false));
253
+ const { resolve, tool } = setup([pausedTool(), pendingApproval()]);
254
+ await resolve("denied");
255
+
256
+ expect(tool()?.toolCall?.status).toBe("running");
257
+ });
258
+
259
+ it("settles a denied call when the accepted denial has no stream body", async () => {
260
+ global.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
261
+ const { resolve, tool } = setup([pausedTool(), pendingApproval("toolu_paused")]);
262
+ await resolve("denied");
263
+
264
+ expect(tool()?.toolCall).toMatchObject({
265
+ status: "complete",
266
+ approvalStatus: "denied",
267
+ success: false,
268
+ error: "Denied",
269
+ });
270
+ });
271
+ });
272
+
273
+ describe("approval settles stay with the stream that decided them", () => {
274
+ const tool = (id: string): AgentWidgetMessage => ({
275
+ ...pausedTool(),
276
+ id: `tool-${id}`,
277
+ toolCall: { ...pausedTool().toolCall!, id },
278
+ });
279
+ const approval = (approvalId: string, toolCallId: string): AgentWidgetMessage => ({
280
+ ...pendingApproval(toolCallId),
281
+ id: `approval-${approvalId}`,
282
+ approval: { ...pendingApproval(toolCallId).approval!, id: approvalId, toolCallId },
283
+ });
284
+ const frame = (event: Record<string, unknown>) =>
285
+ new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`);
286
+
287
+ it("does not settle another stream's paused call when one stream ends", async () => {
288
+ let messages: AgentWidgetMessage[] = [];
289
+ const session = new AgentWidgetSession(
290
+ {
291
+ clientToken: "ct_live_demo",
292
+ apiUrl: "https://api.runtype.com",
293
+ initialMessages: [tool("toolu_a"), approval("appr_a", "toolu_a"), tool("toolu_b"), approval("appr_b", "toolu_b")],
294
+ },
295
+ {
296
+ onMessagesChanged: (m) => { messages = m; },
297
+ onStatusChanged: () => {},
298
+ onStreamingChanged: () => {},
299
+ onError: () => {},
300
+ }
301
+ );
302
+ let controlB!: ReadableStreamDefaultController<Uint8Array>;
303
+ const streamB = new ReadableStream<Uint8Array>({ start: (c) => { controlB = c; } });
304
+ const connectedB = session.connectStream(streamB, { allowReentry: true });
305
+ controlB.enqueue(frame({ type: "approval_complete", executionId: "exec_b", approvalId: "appr_b", decision: "approved" }));
306
+ await new Promise((r) => setTimeout(r, 0));
307
+
308
+ await session.connectStream(
309
+ sse([
310
+ { type: "approval_complete", executionId: "exec_a", approvalId: "appr_a", decision: "approved" },
311
+ { type: "execution_complete", kind: "agent", success: true },
312
+ ]).body!,
313
+ { allowReentry: true }
314
+ );
315
+ const b = () => messages.find((m) => m.toolCall?.id === "toolu_b")?.toolCall;
316
+ expect(b()?.superseded).toBeUndefined();
317
+
318
+ controlB.enqueue(frame({ type: "tool_start", toolCallId: "toolu_b", toolName: "place_pickup_order", toolType: "custom" }));
319
+ controlB.enqueue(frame({ type: "tool_complete", toolCallId: "toolu_b", toolName: "place_pickup_order", success: true, result: { orderId: "ord_b" } }));
320
+ controlB.close();
321
+ await connectedB;
322
+ expect(b()).toMatchObject({ status: "complete", result: { orderId: "ord_b" } });
323
+ expect(b()?.superseded).toBeUndefined();
324
+ });
325
+ });