@runtypelabs/persona 3.34.0 → 3.35.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.
- package/README.md +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +67 -12
- package/src/client.ts +40 -32
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
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",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"import": "./dist/theme-editor.js",
|
|
28
28
|
"require": "./dist/theme-editor.cjs"
|
|
29
29
|
},
|
|
30
|
+
"./theme-editor/preview": {
|
|
31
|
+
"types": "./dist/theme-editor-preview.d.ts",
|
|
32
|
+
"import": "./dist/theme-editor-preview.js",
|
|
33
|
+
"require": "./dist/theme-editor-preview.cjs"
|
|
34
|
+
},
|
|
30
35
|
"./testing": {
|
|
31
36
|
"types": "./dist/testing.d.ts",
|
|
32
37
|
"import": "./dist/testing.js",
|
|
@@ -37,6 +42,11 @@
|
|
|
37
42
|
"import": "./dist/smart-dom-reader.js",
|
|
38
43
|
"require": "./dist/smart-dom-reader.cjs"
|
|
39
44
|
},
|
|
45
|
+
"./voice-worklet-player": {
|
|
46
|
+
"types": "./dist/voice-worklet-player.d.ts",
|
|
47
|
+
"import": "./dist/voice-worklet-player.js",
|
|
48
|
+
"require": "./dist/voice-worklet-player.cjs"
|
|
49
|
+
},
|
|
40
50
|
"./plugin-kit": {
|
|
41
51
|
"types": "./dist/plugin-kit.d.ts",
|
|
42
52
|
"import": "./dist/plugin-kit.js",
|
|
@@ -109,17 +119,21 @@
|
|
|
109
119
|
"access": "public"
|
|
110
120
|
},
|
|
111
121
|
"scripts": {
|
|
112
|
-
"build": "rimraf dist && pnpm run build:styles && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:plugin-kit && pnpm run build:animations",
|
|
122
|
+
"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: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",
|
|
123
|
+
"build:markdown-parsers": "tsup --config tsup.markdown-parsers.config.ts",
|
|
113
124
|
"build:plugin-kit": "tsup src/plugin-kit.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
114
125
|
"build:theme-editor": "tsup src/theme-editor.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
126
|
+
"build:theme-editor-preview": "tsup src/theme-editor-preview.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
115
127
|
"build:testing": "tsup src/testing.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
116
128
|
"build:smart-dom-reader": "tsup src/smart-dom-reader.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
129
|
+
"build:voice-worklet-player": "tsup src/voice-worklet-player.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
|
|
117
130
|
"build:animations": "tsup src/animations/glyph-cycle.ts src/animations/wipe.ts --format esm,cjs --minify --dts --out-dir dist/animations --no-splitting",
|
|
118
131
|
"build:theme-ref": "tsup src/theme-reference.ts --format esm,cjs --minify --dts",
|
|
119
132
|
"build:codegen": "tsup src/codegen.ts --format esm,cjs --minify --dts",
|
|
120
133
|
"build:styles": "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.copyFileSync('src/styles/widget.css','dist/widget.css');\"",
|
|
121
134
|
"build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" && 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);}\"",
|
|
122
135
|
"build:webmcp-polyfill": "tsup --config tsup.webmcp-polyfill.config.ts",
|
|
136
|
+
"build:runtype-tts": "tsup --config tsup.runtype-tts.config.ts",
|
|
123
137
|
"build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
|
|
124
138
|
"build:launcher": "tsup src/launcher-global.ts --format iife --global-name AgentWidgetLauncher --minify --sourcemap --splitting false --out-dir dist && 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);}\"",
|
|
125
139
|
"lint": "eslint . --ext .ts",
|
|
@@ -2,14 +2,13 @@ import type { StreamAnimationPlugin } from "../types";
|
|
|
2
2
|
import { registerStreamAnimationPlugin } from "../utils/stream-animation";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Glyph-cycle animation
|
|
5
|
+
* Glyph-cycle animation: "hacker-settling" reveal.
|
|
6
6
|
*
|
|
7
7
|
* Each arriving char briefly cycles through a sequence of random glyphs
|
|
8
8
|
* before locking in to its final character. Unlike a pure CSS effect, this
|
|
9
9
|
* needs JS because it mutates `textContent` per tick. To keep idiomorph from
|
|
10
10
|
* clobbering mid-cycle work, each cycling span carries
|
|
11
|
-
* `data-preserve-runtime="stream-glyph-cycle"` until its cycle completes
|
|
12
|
-
* morph honors that attribute as an absolute skip marker.
|
|
11
|
+
* `data-preserve-runtime="stream-glyph-cycle"` until its cycle completes: * morph honors that attribute as an absolute skip marker.
|
|
13
12
|
*
|
|
14
13
|
* A short buffer (`BUFFER_THRESHOLD`) holds the bubble empty (with the
|
|
15
14
|
* typing indicator) until enough text has arrived to run a visible reveal.
|
|
@@ -19,7 +18,7 @@ import { registerStreamAnimationPlugin } from "../utils/stream-animation";
|
|
|
19
18
|
* settling wave flows through the full message.
|
|
20
19
|
*
|
|
21
20
|
* Ships as a subpath module so consumers who don't want it pay zero cost.
|
|
22
|
-
* Importing this module auto-registers the plugin globally
|
|
21
|
+
* Importing this module auto-registers the plugin globally: just add the
|
|
23
22
|
* import and set `features.streamAnimation.type = "glyph-cycle"`.
|
|
24
23
|
*
|
|
25
24
|
* ```ts
|
|
@@ -54,8 +53,7 @@ const STYLES = `
|
|
|
54
53
|
const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#$%&@";
|
|
55
54
|
const TICK_COUNT = 10;
|
|
56
55
|
// Base interval between glyph swaps (ms). Scaled by the widget's configured
|
|
57
|
-
// `speed` so the cycle feels coherent with the rest of the stream animation
|
|
58
|
-
// faster speed → snappier cycling, slower speed → more deliberate flicker.
|
|
56
|
+
// `speed` so the cycle feels coherent with the rest of the stream animation:// faster speed → snappier cycling, slower speed → more deliberate flicker.
|
|
59
57
|
const BASE_TICK_MS = 120;
|
|
60
58
|
const DEFAULT_STEP_MS = 120;
|
|
61
59
|
// Probability that each later still-cycling sibling gets re-randomized on
|
|
@@ -63,7 +61,7 @@ const DEFAULT_STEP_MS = 120;
|
|
|
63
61
|
const CROSS_FLICKER_PROBABILITY = 0.4;
|
|
64
62
|
// Hold the stream back until at least this many chars have arrived. Once
|
|
65
63
|
// released, every char is rendered as a random glyph and settles in order
|
|
66
|
-
// from the start
|
|
64
|
+
// from the start, so the visible animation carries through the full text.
|
|
67
65
|
const BUFFER_THRESHOLD = 50;
|
|
68
66
|
|
|
69
67
|
const getStepMs = (container: HTMLElement | null): number => {
|
|
@@ -96,7 +94,7 @@ const flickerLaterSiblings = (span: HTMLElement): void => {
|
|
|
96
94
|
const container = span.closest(".persona-stream-glyph-cycle") as HTMLElement | null;
|
|
97
95
|
if (!container) return;
|
|
98
96
|
// Any scheduled char that still has its final char stashed is "pending
|
|
99
|
-
// or cycling"
|
|
97
|
+
// or cycling": both kinds are valid flicker targets. Settled chars have
|
|
100
98
|
// `data-glyph-cycle-final` deleted and are excluded.
|
|
101
99
|
const unsettled = container.querySelectorAll<HTMLElement>(
|
|
102
100
|
".persona-stream-char[data-glyph-cycle-final]"
|
|
@@ -115,8 +113,8 @@ const flickerLaterSiblings = (span: HTMLElement): void => {
|
|
|
115
113
|
};
|
|
116
114
|
|
|
117
115
|
// Per-container timestamp tracking: when the next char's cycle should settle.
|
|
118
|
-
// Each char starts cycling immediately on schedule
|
|
119
|
-
// wait
|
|
116
|
+
// Each char starts cycling immediately on schedule: no static placeholder
|
|
117
|
+
// wait, and finishes when `Date.now() >= settleAt`. This slot advances by
|
|
120
118
|
// one stagger step per schedule, so the burst settles left-to-right while
|
|
121
119
|
// every char is visibly flickering from the moment it enters the DOM.
|
|
122
120
|
const containerNextSettleAt = new WeakMap<Element, number>();
|
|
@@ -139,8 +137,7 @@ const observeStagger = (container: Element | null, now: number): number => {
|
|
|
139
137
|
let observed = containerArrivalMs.get(container) ?? DEFAULT_ARRIVAL_MS;
|
|
140
138
|
if (last !== undefined) {
|
|
141
139
|
const interval = now - last;
|
|
142
|
-
// Skip near-zero intervals (burst within one MutationObserver batch)
|
|
143
|
-
// they don't reflect the real stream cadence.
|
|
140
|
+
// Skip near-zero intervals (burst within one MutationObserver batch): // they don't reflect the real stream cadence.
|
|
144
141
|
if (interval > 1) {
|
|
145
142
|
observed = observed * (1 - EMA_ALPHA) + interval * EMA_ALPHA;
|
|
146
143
|
containerArrivalMs.set(container, observed);
|
|
@@ -180,7 +177,7 @@ const scheduleCycle = (span: HTMLElement): void => {
|
|
|
180
177
|
span.dataset.glyphCycleScheduled = "true";
|
|
181
178
|
// Stash the target char and immediately paint a random glyph so the span
|
|
182
179
|
// reads as "cycling" even before its staggered kickoff. Also opt out of
|
|
183
|
-
// morph
|
|
180
|
+
// morph: without this, streamed token re-renders would overwrite our
|
|
184
181
|
// placeholder glyph with the final char.
|
|
185
182
|
span.dataset.glyphCycleFinal = finalChar;
|
|
186
183
|
span.setAttribute("data-preserve-runtime", "stream-glyph-cycle");
|
|
@@ -275,7 +272,7 @@ export const glyphCycle: StreamAnimationPlugin = {
|
|
|
275
272
|
// `**template literal**`) would trigger a render of the partial
|
|
276
273
|
// `**template`, which markdown emits as literal asterisks, gets
|
|
277
274
|
// wrapped + marked `data-preserve-runtime`, and leaks through later
|
|
278
|
-
// morphs
|
|
275
|
+
// morphs: the final structure can't reconcile.
|
|
279
276
|
let boldPairs = 0;
|
|
280
277
|
let lastSafe = -1;
|
|
281
278
|
let i = 0;
|
package/src/animations/wipe.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { StreamAnimationPlugin } from "../types";
|
|
|
2
2
|
import { registerStreamAnimationPlugin } from "../utils/stream-animation";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Wipe animation
|
|
5
|
+
* Wipe animation: per-word left-to-right mask reveal.
|
|
6
6
|
*
|
|
7
7
|
* Each arriving word is revealed via a soft feathered mask that sweeps from
|
|
8
8
|
* right to left. Uses `mask-image` (not `background-clip: text`) so the
|
|
@@ -137,7 +137,7 @@ describe("AgentWidgetClient - built-in ask_user_question exposure", () => {
|
|
|
137
137
|
|
|
138
138
|
it("changes the clientTools fingerprint when toggled (diff-only resend)", () => {
|
|
139
139
|
// In client-token mode the widget only resends the full clientTools[]
|
|
140
|
-
// when the fingerprint changes
|
|
140
|
+
// when the fingerprint changes: toggling expose must change it so the
|
|
141
141
|
// server actually learns about (or forgets) the built-in tool.
|
|
142
142
|
const webMcpOnly = [{ name: "search", description: "s" }];
|
|
143
143
|
const withBuiltIn = [ASK_USER_QUESTION_CLIENT_TOOL, ...webMcpOnly];
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Built-in `ask_user_question` client tool.
|
|
3
3
|
*
|
|
4
4
|
* The widget can advertise this tool to the agent on every dispatch via
|
|
5
|
-
* `clientTools[]` (set `features.askUserQuestion.expose: true`)
|
|
5
|
+
* `clientTools[]` (set `features.askUserQuestion.expose: true`): the same
|
|
6
6
|
* wire surface WebMCP page tools ride. The server registers it as a LOCAL
|
|
7
7
|
* tool under its bare name (`origin: 'sdk'` tools are not `webmcp:`-prefixed),
|
|
8
8
|
* so when the model calls it the execution pauses with a `step_await`
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* This replaces the previous integrator burden of hand-declaring the tool in
|
|
14
14
|
* a flow's `runtimeTools` and keeping that schema in sync with the renderer.
|
|
15
15
|
* Flows that already declare `ask_user_question` server-side should leave
|
|
16
|
-
* `expose` off
|
|
16
|
+
* `expose` off: the model would otherwise see the tool twice.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import {
|
|
@@ -28,8 +28,7 @@ import type { AgentWidgetConfig, ClientToolDefinition } from "./types";
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* JSON Schema for the tool's parameters. Mirrors {@link AskUserQuestionPayload}
|
|
31
|
-
|
|
32
|
-
* so the schema the model is held to and the schema the renderer expects can
|
|
31
|
+
*, the shape `parseAskUserQuestionPayload` hydrates the answer sheet from, * so the schema the model is held to and the schema the renderer expects can
|
|
33
32
|
* never drift.
|
|
34
33
|
*/
|
|
35
34
|
export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
|
|
@@ -57,7 +56,7 @@ export const ASK_USER_QUESTION_PARAMETERS_SCHEMA = {
|
|
|
57
56
|
minItems: 2,
|
|
58
57
|
maxItems: 4,
|
|
59
58
|
description:
|
|
60
|
-
'2-4 distinct choices. Do NOT add an "Other" option
|
|
59
|
+
'2-4 distinct choices. Do NOT add an "Other" option: free text is automatic.',
|
|
61
60
|
items: {
|
|
62
61
|
type: "object",
|
|
63
62
|
properties: {
|
|
@@ -102,7 +101,7 @@ export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
|
|
|
102
101
|
name: ASK_USER_QUESTION_TOOL_NAME,
|
|
103
102
|
description:
|
|
104
103
|
"Ask the user multiple-choice questions and wait for their answers. Use " +
|
|
105
|
-
"only when blocked on a decision that is the user's to make
|
|
104
|
+
"only when blocked on a decision that is the user's to make: a " +
|
|
106
105
|
"preference, a choice between valid approaches, or information you " +
|
|
107
106
|
"cannot infer. Each question offers 2-4 options plus an automatic " +
|
|
108
107
|
"free-text input. The result maps each question to its answer (an array " +
|
|
@@ -119,8 +118,7 @@ export const ASK_USER_QUESTION_CLIENT_TOOL: ClientToolDefinition = {
|
|
|
119
118
|
*
|
|
120
119
|
* Each tool is gated on BOTH of its feature flags: `expose` opts the tool
|
|
121
120
|
* into the agent's catalog, and `enabled !== false` guarantees the widget can
|
|
122
|
-
* actually render UI (and, for fire-and-forget tools, auto-resume) for it
|
|
123
|
-
* exposing a tool with its feature disabled would park the execution on a
|
|
121
|
+
* actually render UI (and, for fire-and-forget tools, auto-resume) for it: * exposing a tool with its feature disabled would park the execution on a
|
|
124
122
|
* generic tool bubble with no way to advance.
|
|
125
123
|
*/
|
|
126
124
|
export const builtInClientToolsForDispatch = (
|
package/src/client.test.ts
CHANGED
|
@@ -2348,7 +2348,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
|
2348
2348
|
};
|
|
2349
2349
|
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2350
2350
|
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2351
|
-
// No seq field
|
|
2351
|
+
// No seq field: should append in arrival order
|
|
2352
2352
|
e({ type: 'step_delta', id: 's1', text: 'Hello ' });
|
|
2353
2353
|
e({ type: 'step_delta', id: 's1', text: 'world' });
|
|
2354
2354
|
e({ type: 'step_delta', id: 's1', text: '!' });
|
|
@@ -2386,7 +2386,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
|
2386
2386
|
};
|
|
2387
2387
|
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2388
2388
|
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2389
|
-
// seq=3 arrives first (leading gap
|
|
2389
|
+
// seq=3 arrives first (leading gap: seq 1 and 2 arrive later)
|
|
2390
2390
|
e({ type: 'step_delta', id: 's1', text: 'c', partId: 'text_0', seq: 3 });
|
|
2391
2391
|
e({ type: 'step_delta', id: 's1', text: 'a', partId: 'text_0', seq: 1 });
|
|
2392
2392
|
e({ type: 'step_delta', id: 's1', text: 'b', partId: 'text_0', seq: 2 });
|
|
@@ -2425,7 +2425,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
|
2425
2425
|
};
|
|
2426
2426
|
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2427
2427
|
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2428
|
-
// reason_delta uses sequenceIndex, step_delta uses seq
|
|
2428
|
+
// reason_delta uses sequenceIndex, step_delta uses seq: same counter
|
|
2429
2429
|
e({ type: 'reason_start', reasoningId: 'r1', hidden: false, done: false });
|
|
2430
2430
|
e({ type: 'reason_delta', reasoningId: 'r1', reasoningText: 'thinking', hidden: false, done: false, sequenceIndex: 1 });
|
|
2431
2431
|
e({ type: 'reason_complete', reasoningId: 'r1', hidden: false, done: true });
|
|
@@ -2518,7 +2518,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
|
2518
2518
|
};
|
|
2519
2519
|
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2520
2520
|
e({ type: 'step_start', id: 's1', name: 'Prompt', stepType: 'prompt', index: 1, totalSteps: 1 });
|
|
2521
|
-
// Only a seq=3 event arrives
|
|
2521
|
+
// Only a seq=3 event arrives: seq=1 and seq=2 are never delivered.
|
|
2522
2522
|
// Without the end-of-stream flush, this event would be stranded in
|
|
2523
2523
|
// the reorder buffer and never emitted.
|
|
2524
2524
|
e({ type: 'step_delta', id: 's1', text: 'tail', partId: 'text_0', seq: 3 });
|
|
@@ -2607,7 +2607,7 @@ describe('AgentWidgetClient - Out-of-Order Sequence Reordering', () => {
|
|
|
2607
2607
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
2608
2608
|
};
|
|
2609
2609
|
e({ type: 'flow_start', flowId: 'f1', flowName: 'Test', totalSteps: 1 });
|
|
2610
|
-
// Only sequenced event
|
|
2610
|
+
// Only sequenced event, but seq > 1 so it would be buffered.
|
|
2611
2611
|
e({ type: 'error', error: 'boom', seq: 2 });
|
|
2612
2612
|
controller.close();
|
|
2613
2613
|
},
|
|
@@ -2767,7 +2767,7 @@ describe('AgentWidgetClient - stopReason propagation', () => {
|
|
|
2767
2767
|
});
|
|
2768
2768
|
|
|
2769
2769
|
// ============================================================================
|
|
2770
|
-
// Within-turn text/tool interleaving
|
|
2770
|
+
// Within-turn text/tool interleaving: assistant text bubbles must seal at
|
|
2771
2771
|
// each agent_tool_start so the chronological text→tool→text→tool sequence
|
|
2772
2772
|
// renders as distinct timeline entries instead of one merged bubble that
|
|
2773
2773
|
// appears below all the tool cards.
|
|
@@ -3004,7 +3004,7 @@ describe('AgentWidgetClient - agent_turn text/tool interleaving', () => {
|
|
|
3004
3004
|
// step_await (LOCAL tool pause) + resumeFlow
|
|
3005
3005
|
// ============================================================================
|
|
3006
3006
|
|
|
3007
|
-
describe('AgentWidgetClient
|
|
3007
|
+
describe('AgentWidgetClient: step_await parsing', () => {
|
|
3008
3008
|
const buildStepAwaitStream = (payload: Record<string, unknown>): ReadableStream<Uint8Array> => {
|
|
3009
3009
|
const encoder = new TextEncoder();
|
|
3010
3010
|
const body = `event: step_await\ndata: ${JSON.stringify({ type: 'step_await', ...payload })}\n\n`;
|
|
@@ -3196,7 +3196,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
|
|
|
3196
3196
|
|
|
3197
3197
|
await client.resumeFlow('exec_xyz', { toolu_A: { ok: true } });
|
|
3198
3198
|
|
|
3199
|
-
// Session-authed sibling of /v1/client/chat
|
|
3199
|
+
// Session-authed sibling of /v1/client/chat: no Bearer key, sessionId in body.
|
|
3200
3200
|
expect(capturedUrl).toBe('https://api.runtype.com/v1/client/resume');
|
|
3201
3201
|
expect(capturedBody).toEqual({
|
|
3202
3202
|
executionId: 'exec_xyz',
|
|
@@ -3239,7 +3239,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
|
|
|
3239
3239
|
clientToken: 'ct_live_demo',
|
|
3240
3240
|
apiUrl: 'https://api.runtype.com',
|
|
3241
3241
|
});
|
|
3242
|
-
// A stale session that has already expired
|
|
3242
|
+
// A stale session that has already expired: a long WebMCP approval wait can
|
|
3243
3243
|
// outlive it, so resumeFlow must not trust this.clientSession directly.
|
|
3244
3244
|
(client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
|
|
3245
3245
|
sessionId: 'cs_stale',
|
|
@@ -3647,7 +3647,7 @@ describe('AgentWidgetClient - agent_media events', () => {
|
|
|
3647
3647
|
sseEvent('agent_iteration_start', { executionId: execId, iteration: 1, maxTurns: 1, startedAt: new Date().toISOString(), seq: 2 }),
|
|
3648
3648
|
sseEvent('agent_turn_start', { executionId: execId, iteration: 1, turnIndex: 0, role: 'assistant', turnId: 'turn_1', seq: 3 }),
|
|
3649
3649
|
sseEvent('agent_turn_delta', { executionId: execId, iteration: 1, delta: 'Streaming...', contentType: 'text', turnId: 'turn_1', seq: 4 }),
|
|
3650
|
-
// Media arrives mid-stream
|
|
3650
|
+
// Media arrives mid-stream: earlier text bubble is still streaming.
|
|
3651
3651
|
sseEvent('agent_media', {
|
|
3652
3652
|
executionId: execId, iteration: 1, toolCallId: 'tc_seal', toolName: 'shot',
|
|
3653
3653
|
media: [{ type: 'media', data: 'PNG', mediaType: 'image/png' }],
|
|
@@ -3755,7 +3755,7 @@ describe('AgentWidgetClient - agent_media events', () => {
|
|
|
3755
3755
|
sseEvent('agent_iteration_start', { executionId: execId, iteration: 1, maxTurns: 1, startedAt: new Date().toISOString(), seq: 2 }),
|
|
3756
3756
|
sseEvent('agent_media', {
|
|
3757
3757
|
executionId: execId, iteration: 1, toolCallId: 'tc_blob', toolName: 'opaque',
|
|
3758
|
-
// mediaType is empty
|
|
3758
|
+
// mediaType is empty: should not produce a malformed `data:;base64,...` URI.
|
|
3759
3759
|
media: [{ type: 'media', data: 'AAAA', mediaType: '' }],
|
|
3760
3760
|
seq: 3,
|
|
3761
3761
|
}),
|
|
@@ -4004,7 +4004,7 @@ describe('AgentWidgetClient - diff-only clientTools (client-token)', () => {
|
|
|
4004
4004
|
return sse();
|
|
4005
4005
|
});
|
|
4006
4006
|
const client = makeClient(TOOLS);
|
|
4007
|
-
// Pre-seed the cache so the first attempt would be fingerprint-only
|
|
4007
|
+
// Pre-seed the cache so the first attempt would be fingerprint-only: the
|
|
4008
4008
|
// server then forces a resend.
|
|
4009
4009
|
(client as unknown as { lastSentClientToolsFingerprint: string | null; clientToolsFingerprintSessionId: string | null }).lastSentClientToolsFingerprint =
|
|
4010
4010
|
'stale';
|
|
@@ -4121,3 +4121,58 @@ describe('AgentWidgetClient - non-client-token paths always send full clientTool
|
|
|
4121
4121
|
}
|
|
4122
4122
|
});
|
|
4123
4123
|
});
|
|
4124
|
+
|
|
4125
|
+
describe('AgentWidgetClient - Feedback request builder', () => {
|
|
4126
|
+
const INIT_RESPONSE = {
|
|
4127
|
+
sessionId: 'sess_123',
|
|
4128
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
4129
|
+
flow: {},
|
|
4130
|
+
config: { welcomeMessage: 'hi', placeholder: 'type…', theme: {} },
|
|
4131
|
+
};
|
|
4132
|
+
|
|
4133
|
+
function mockInitAndFeedback() {
|
|
4134
|
+
const feedbackBodies: Array<Record<string, unknown>> = [];
|
|
4135
|
+
global.fetch = vi.fn().mockImplementation(async (url: string, options: { body: string }) => {
|
|
4136
|
+
if (typeof url === 'string' && url.endsWith('/v1/client/feedback')) {
|
|
4137
|
+
feedbackBodies.push(JSON.parse(options.body));
|
|
4138
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
4139
|
+
}
|
|
4140
|
+
// init request
|
|
4141
|
+
return { ok: true, status: 200, json: async () => INIT_RESPONSE };
|
|
4142
|
+
});
|
|
4143
|
+
return feedbackBodies;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
it('includes the client token in the feedback request body', async () => {
|
|
4147
|
+
const feedbackBodies = mockInitAndFeedback();
|
|
4148
|
+
const client = new AgentWidgetClient({
|
|
4149
|
+
apiUrl: 'http://localhost:8000',
|
|
4150
|
+
clientToken: 'ct_live_abc123',
|
|
4151
|
+
});
|
|
4152
|
+
await client.initSession();
|
|
4153
|
+
|
|
4154
|
+
await client.sendFeedback({ sessionId: 'sess_123', messageId: 'm1', type: 'upvote' });
|
|
4155
|
+
|
|
4156
|
+
expect(feedbackBodies).toHaveLength(1);
|
|
4157
|
+
expect(feedbackBodies[0]).toEqual({
|
|
4158
|
+
sessionId: 'sess_123',
|
|
4159
|
+
messageId: 'm1',
|
|
4160
|
+
type: 'upvote',
|
|
4161
|
+
token: 'ct_live_abc123',
|
|
4162
|
+
});
|
|
4163
|
+
});
|
|
4164
|
+
|
|
4165
|
+
it('sends the token on csat/nps submissions too', async () => {
|
|
4166
|
+
const feedbackBodies = mockInitAndFeedback();
|
|
4167
|
+
const client = new AgentWidgetClient({
|
|
4168
|
+
apiUrl: 'http://localhost:8000',
|
|
4169
|
+
clientToken: 'ct_live_xyz789',
|
|
4170
|
+
});
|
|
4171
|
+
await client.initSession();
|
|
4172
|
+
|
|
4173
|
+
await client.sendFeedback({ sessionId: 'sess_123', type: 'csat', rating: 5 });
|
|
4174
|
+
|
|
4175
|
+
expect(feedbackBodies[0].token).toBe('ct_live_xyz789');
|
|
4176
|
+
expect(feedbackBodies[0].rating).toBe(5);
|
|
4177
|
+
});
|
|
4178
|
+
});
|