@runtypelabs/persona 4.21.1 → 4.22.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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
- package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +136 -6
- package/dist/animations-extra.d.ts +136 -6
- package/dist/{approval-ui-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +221 -16
- package/dist/approval-ui.d.ts +221 -16
- package/dist/approval-ui.js +2 -2
- package/dist/artifacts-ui-F4N6VMKF.js +7 -0
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +221 -16
- package/dist/artifacts-ui.d.ts +221 -16
- package/dist/artifacts-ui.js +1 -1
- package/dist/chunk-NQLDCJAP.js +4 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +221 -16
- package/dist/context-mentions.d.ts +221 -16
- package/dist/event-stream-view-GL3ADRY2.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +221 -16
- package/dist/event-stream-view.d.ts +221 -16
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +234 -25
- package/dist/forms-ui.d.ts +234 -25
- package/dist/history-view.d.cts +136 -6
- package/dist/history-view.d.ts +136 -6
- package/dist/index.cjs +70 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +302 -32
- package/dist/index.d.ts +302 -32
- package/dist/index.global.js +54 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +69 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +221 -16
- package/dist/session-reconnect.d.ts +221 -16
- package/dist/smart-dom-reader.d.cts +221 -16
- package/dist/smart-dom-reader.d.ts +221 -16
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -4
- package/dist/testing.d.ts +3 -4
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +73 -73
- package/dist/theme-editor-preview.d.cts +221 -16
- package/dist/theme-editor-preview.d.ts +221 -16
- package/dist/theme-editor-preview.js +51 -51
- package/dist/theme-editor.cjs +4 -4
- package/dist/theme-editor.d.cts +221 -16
- package/dist/theme-editor.d.ts +221 -16
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +65 -1
- package/dist/theme-reference.d.ts +65 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-runtime.d.cts +6 -2
- package/dist/voice-runtime.d.ts +6 -2
- package/dist/webmcp-polyfill.js +1 -4
- package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +34 -2
- package/dist/webmcp-runtime.d.ts +34 -2
- package/dist/webmcp-runtime.js +2 -2
- package/package.json +2 -2
- package/src/ask-user-question-tool.ts +1 -1
- package/src/client-history-rest.test.ts +2 -1
- package/src/client-visitor-history.test.ts +20 -4
- package/src/client.composer-options.test.ts +1 -1
- package/src/client.interrupt.test.ts +2 -2
- package/src/client.test.ts +477 -915
- package/src/client.ts +134 -494
- package/src/client.unified-tools.test.ts +76 -0
- package/src/components/artifact-preview.ts +5 -0
- package/src/components/event-stream-badge-theming.test.ts +162 -0
- package/src/components/event-stream-view.test.ts +76 -76
- package/src/components/event-stream-view.ts +55 -20
- package/src/components/tool-bubble.test.ts +12 -0
- package/src/components/tool-bubble.ts +13 -2
- package/src/event-stream-view-bundle.test.ts +2 -2
- package/src/generated/runtype-openapi-contract.ts +137 -1
- package/src/index-core.ts +2 -0
- package/src/internal/runtype-history-provider.test.ts +2 -2
- package/src/runtime/init.registry.test.ts +56 -0
- package/src/runtime/init.ts +17 -0
- package/src/session-history.test.ts +1 -1
- package/src/session.quote.test.ts +1 -1
- package/src/session.resubmit-from.test.ts +1 -1
- package/src/session.test.ts +5 -5
- package/src/session.ts +95 -36
- package/src/suggest-replies-tool.ts +1 -1
- package/src/testing/mock-stream.test.ts +37 -16
- package/src/testing/mock-stream.ts +14 -9
- package/src/types/theme.ts +41 -1
- package/src/types.ts +57 -15
- package/src/ui.ask-user-question-plugin.test.ts +7 -7
- package/src/ui.composer-defer-one.test.ts +6 -34
- package/src/ui.composer-draft-persistence.test.ts +1 -1
- package/src/ui.composer-quote.test.ts +1 -1
- package/src/ui.message-history-actions.test.ts +1 -1
- package/src/ui.reasoning-display.test.ts +136 -2
- package/src/ui.streaming-coalescing.test.ts +10 -13
- package/src/ui.ts +101 -35
- package/src/ui.typing-indicator.test.ts +216 -0
- package/src/ui.voice-custom-provider.test.ts +211 -0
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
- package/src/utils/event-stream-buffer.test.ts +13 -13
- package/src/utils/event-stream-capture.test.ts +20 -20
- package/src/utils/formatting.ts +22 -0
- package/src/utils/theme.ts +16 -0
- package/src/utils/throughput-tracker.test.ts +100 -142
- package/src/utils/throughput-tracker.ts +18 -132
- package/src/utils/tokens.ts +59 -0
- package/src/utils/voice-support.ts +18 -1
- package/src/voice/voice-turn-tracker.ts +18 -0
- package/src/webmcp-bridge.test.ts +132 -23
- package/src/webmcp-bridge.ts +27 -3
- package/src/webmcp-runtime-entry.ts +71 -11
- package/dist/artifacts-ui-J6AKUGTS.js +0 -7
- package/dist/chunk-HBE7LFNT.js +0 -4
- package/dist/event-stream-view-73NWKWVQ.js +0 -1
- package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
- package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// then prefers exact provider usage (output tokens) when terminal events carry
|
|
13
13
|
// it. A run starts when the stream starts (or lazily on the first visible
|
|
14
14
|
// delta), stays "running" across intermediate step/turn completions, and only
|
|
15
|
-
// finalizes on terminal `
|
|
15
|
+
// finalizes on terminal `execution_complete`. Stream errors mark
|
|
16
16
|
// the metric unavailable rather than leaving it stuck "running".
|
|
17
17
|
|
|
18
18
|
export type ThroughputMetricStatus = "idle" | "running" | "complete" | "error";
|
|
@@ -47,47 +47,10 @@ interface ThroughputRunStats {
|
|
|
47
47
|
// execution time or whole-request duration instead.
|
|
48
48
|
const THROUGHPUT_MIN_DURATION_MS = 250;
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// already in progress, so a prior stream that ended without a terminal/error
|
|
55
|
-
// frame (e.g. `session.cancel()`) doesn't bleed its tokens into the next one.
|
|
56
|
-
const REQUEST_START_EVENTS = new Set([
|
|
57
|
-
"flow_start",
|
|
58
|
-
"flow_run_start",
|
|
59
|
-
"agent_start",
|
|
60
|
-
"dispatch_start",
|
|
61
|
-
"run_start",
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
// Per-step markers that fire repeatedly WITHIN a single request (a flow emits
|
|
65
|
-
// one per step). These only lazily begin a run: they must never reset, or a
|
|
66
|
-
// multi-step response would restart the metric between steps. If no request- or
|
|
67
|
-
// step-start event is emitted, the first visible delta lazily starts the run.
|
|
68
|
-
const STEP_START_EVENTS = new Set(["step_start", "execution_start"]);
|
|
69
|
-
|
|
70
|
-
const VISIBLE_DELTA_EVENTS = new Set([
|
|
71
|
-
"step_delta",
|
|
72
|
-
"step_chunk",
|
|
73
|
-
"chunk",
|
|
74
|
-
"agent_turn_delta",
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
const INTERMEDIATE_COMPLETE_EVENTS = new Set([
|
|
78
|
-
"step_complete",
|
|
79
|
-
"agent_turn_complete",
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
const TERMINAL_COMPLETE_EVENTS = new Set(["flow_complete", "agent_complete"]);
|
|
83
|
-
|
|
84
|
-
const ERROR_EVENTS = new Set([
|
|
85
|
-
"step_error",
|
|
86
|
-
"flow_error",
|
|
87
|
-
"agent_error",
|
|
88
|
-
"dispatch_error",
|
|
89
|
-
"error",
|
|
90
|
-
]);
|
|
50
|
+
// Execution starts reset a stale/cancelled run; step and turn starts only
|
|
51
|
+
// lazily initialize it, since they can repeat within the same execution.
|
|
52
|
+
const STEP_START_EVENTS = new Set(["step_start", "turn_start"]);
|
|
53
|
+
const INTERMEDIATE_COMPLETE_EVENTS = new Set(["step_complete", "turn_complete"]);
|
|
91
54
|
|
|
92
55
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
93
56
|
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -134,91 +97,15 @@ function resolveEventType(
|
|
|
134
97
|
return typeof payload.type === "string" ? payload.type : eventType;
|
|
135
98
|
}
|
|
136
99
|
|
|
137
|
-
|
|
138
|
-
if (typeof payload.text === "string") return payload.text;
|
|
139
|
-
if (typeof payload.delta === "string") return payload.delta;
|
|
140
|
-
if (typeof payload.content === "string") return payload.content;
|
|
141
|
-
if (typeof payload.chunk === "string") return payload.chunk;
|
|
142
|
-
return "";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Only count visible model output.
|
|
147
|
-
*
|
|
148
|
-
* For `agent_turn_delta`, count only a contentType of exactly `text` as
|
|
149
|
-
* visible, matching the client renderer (which appends streaming assistant
|
|
150
|
-
* text only when `contentType === "text"`); `thinking`, `tool_input`, any
|
|
151
|
-
* other value, and a missing contentType are ignored so throughput never
|
|
152
|
-
* includes deltas the chat UI doesn't render.
|
|
153
|
-
*
|
|
154
|
-
* For `step_delta` / `step_chunk`, skip tool and context steps: those carry
|
|
155
|
-
* tool I/O, not model-visible text: mirroring the widget's own renderer.
|
|
156
|
-
*/
|
|
157
|
-
function isVisibleTextDelta(
|
|
158
|
-
type: string,
|
|
159
|
-
payload: Record<string, unknown>
|
|
160
|
-
): boolean {
|
|
161
|
-
if (type === "step_delta" || type === "step_chunk") {
|
|
162
|
-
return payload.stepType !== "tool" && payload.executionType !== "context";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (type !== "agent_turn_delta") return true;
|
|
166
|
-
|
|
167
|
-
const contentType =
|
|
168
|
-
typeof payload.contentType === "string"
|
|
169
|
-
? payload.contentType
|
|
170
|
-
: typeof payload.content_type === "string"
|
|
171
|
-
? payload.content_type
|
|
172
|
-
: undefined;
|
|
173
|
-
|
|
174
|
-
return contentType === "text";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Extract exact output tokens from a variety of usage payload shapes. */
|
|
100
|
+
/** Exact output counts are carried by turn and execution completions. */
|
|
178
101
|
function getOutputTokens(payload: Record<string, unknown>): number | undefined {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
getRecord(payload, "tokens"),
|
|
182
|
-
getRecord(payload, "totalTokens"),
|
|
183
|
-
result ? getRecord(result, "tokens") : undefined,
|
|
184
|
-
getRecord(payload, "usage"),
|
|
185
|
-
result ? getRecord(result, "usage") : undefined,
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
for (const candidate of candidates) {
|
|
189
|
-
if (!candidate) continue;
|
|
190
|
-
const outputTokens =
|
|
191
|
-
toFiniteNumber(candidate.output) ??
|
|
192
|
-
toFiniteNumber(candidate.outputTokens) ??
|
|
193
|
-
toFiniteNumber(candidate.completionTokens);
|
|
194
|
-
if (outputTokens !== undefined) return outputTokens;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
toFiniteNumber(payload.outputTokens) ??
|
|
199
|
-
toFiniteNumber(payload.completionTokens) ??
|
|
200
|
-
(result
|
|
201
|
-
? (toFiniteNumber(result.outputTokens) ??
|
|
202
|
-
toFiniteNumber(result.completionTokens))
|
|
203
|
-
: undefined)
|
|
204
|
-
);
|
|
102
|
+
const tokens = getRecord(payload, "tokens") ?? getRecord(payload, "totalTokens");
|
|
103
|
+
return tokens ? toFiniteNumber(tokens.output) : undefined;
|
|
205
104
|
}
|
|
206
105
|
|
|
207
|
-
/**
|
|
208
|
-
function getExecutionTimeMs(
|
|
209
|
-
payload
|
|
210
|
-
): number | undefined {
|
|
211
|
-
const result = getRecord(payload, "result");
|
|
212
|
-
return (
|
|
213
|
-
toFiniteNumber(payload.executionTime) ??
|
|
214
|
-
toFiniteNumber(payload.executionTimeMs) ??
|
|
215
|
-
toFiniteNumber(payload.execution_time) ??
|
|
216
|
-
toFiniteNumber(payload.duration) ??
|
|
217
|
-
(result
|
|
218
|
-
? (toFiniteNumber(result.executionTime) ??
|
|
219
|
-
toFiniteNumber(result.executionTimeMs))
|
|
220
|
-
: undefined)
|
|
221
|
-
);
|
|
106
|
+
/** Current step/execution completions report elapsed milliseconds as durationMs. */
|
|
107
|
+
function getExecutionTimeMs(payload: Record<string, unknown>): number | undefined {
|
|
108
|
+
return toFiniteNumber(payload.durationMs);
|
|
222
109
|
}
|
|
223
110
|
|
|
224
111
|
function defaultClock(): number {
|
|
@@ -286,8 +173,8 @@ export class ThroughputTracker {
|
|
|
286
173
|
|
|
287
174
|
processEvent(eventType: string, payload: unknown): void {
|
|
288
175
|
if (!isRecord(payload)) {
|
|
289
|
-
// Non-object payloads can still signal lifecycle (e.g. bare "
|
|
290
|
-
if (
|
|
176
|
+
// Non-object payloads can still signal lifecycle (e.g. bare "execution_error").
|
|
177
|
+
if (eventType === "execution_error" && this.run) {
|
|
291
178
|
this.run = null;
|
|
292
179
|
this.metric = { status: "error" };
|
|
293
180
|
}
|
|
@@ -297,7 +184,7 @@ export class ThroughputTracker {
|
|
|
297
184
|
const type = resolveEventType(eventType, payload);
|
|
298
185
|
const now = this.now();
|
|
299
186
|
|
|
300
|
-
if (
|
|
187
|
+
if (type === "execution_start") {
|
|
301
188
|
// New request: start fresh, discarding any incomplete prior run.
|
|
302
189
|
this.startRun(now);
|
|
303
190
|
return;
|
|
@@ -309,9 +196,8 @@ export class ThroughputTracker {
|
|
|
309
196
|
return;
|
|
310
197
|
}
|
|
311
198
|
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
const text = getTextDelta(payload);
|
|
199
|
+
if (type === "text_delta") {
|
|
200
|
+
const text = typeof payload.delta === "string" ? payload.delta : "";
|
|
315
201
|
if (!text) return;
|
|
316
202
|
|
|
317
203
|
// Lazily start a run if the stream began without a recognized start event.
|
|
@@ -367,7 +253,7 @@ export class ThroughputTracker {
|
|
|
367
253
|
return;
|
|
368
254
|
}
|
|
369
255
|
|
|
370
|
-
if (
|
|
256
|
+
if (type === "execution_complete") {
|
|
371
257
|
if (!this.run) return;
|
|
372
258
|
const stats = this.run;
|
|
373
259
|
// Prefer exact output tokens from this terminal event, else accumulated
|
|
@@ -396,7 +282,7 @@ export class ThroughputTracker {
|
|
|
396
282
|
return;
|
|
397
283
|
}
|
|
398
284
|
|
|
399
|
-
if (
|
|
285
|
+
if (type === "execution_error" || (type === "error" && payload.recoverable === false)) {
|
|
400
286
|
if (!this.run) return;
|
|
401
287
|
this.run = null;
|
|
402
288
|
this.metric = { status: "error" };
|
package/src/utils/tokens.ts
CHANGED
|
@@ -128,6 +128,32 @@ export const DEFAULT_PALETTE = {
|
|
|
128
128
|
900: '#1e3a8a',
|
|
129
129
|
950: '#172554',
|
|
130
130
|
},
|
|
131
|
+
purple: {
|
|
132
|
+
50: '#faf5ff',
|
|
133
|
+
100: '#f3e8ff',
|
|
134
|
+
200: '#e9d5ff',
|
|
135
|
+
300: '#d8b4fe',
|
|
136
|
+
400: '#c084fc',
|
|
137
|
+
500: '#a855f7',
|
|
138
|
+
600: '#9333ea',
|
|
139
|
+
700: '#7e22ce',
|
|
140
|
+
800: '#6b21a8',
|
|
141
|
+
900: '#581c87',
|
|
142
|
+
950: '#3b0764',
|
|
143
|
+
},
|
|
144
|
+
teal: {
|
|
145
|
+
50: '#f0fdfa',
|
|
146
|
+
100: '#ccfbf1',
|
|
147
|
+
200: '#99f6e4',
|
|
148
|
+
300: '#5eead4',
|
|
149
|
+
400: '#2dd4bf',
|
|
150
|
+
500: '#14b8a6',
|
|
151
|
+
600: '#0d9488',
|
|
152
|
+
700: '#0f766e',
|
|
153
|
+
800: '#115e59',
|
|
154
|
+
900: '#134e4a',
|
|
155
|
+
950: '#042f2e',
|
|
156
|
+
},
|
|
131
157
|
},
|
|
132
158
|
spacing,
|
|
133
159
|
typography: {
|
|
@@ -472,6 +498,20 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
472
498
|
border: 'palette.colors.gray.200',
|
|
473
499
|
},
|
|
474
500
|
},
|
|
501
|
+
// Event-stream badge chips: 100-tone fill / dark text per family. tool/agent
|
|
502
|
+
// keep their pre-token literals (#6b21a8 / #115e59), which sit at the 800
|
|
503
|
+
// step of the canonical purple/teal ramps.
|
|
504
|
+
eventStream: {
|
|
505
|
+
badge: {
|
|
506
|
+
flow: { background: 'palette.colors.success.100', foreground: 'palette.colors.success.700' },
|
|
507
|
+
step: { background: 'palette.colors.primary.100', foreground: 'palette.colors.primary.700' },
|
|
508
|
+
reasoning: { background: 'palette.colors.warning.100', foreground: 'palette.colors.warning.700' },
|
|
509
|
+
tool: { background: 'palette.colors.purple.100', foreground: 'palette.colors.purple.800' },
|
|
510
|
+
agent: { background: 'palette.colors.teal.100', foreground: 'palette.colors.teal.800' },
|
|
511
|
+
error: { background: 'palette.colors.error.100', foreground: 'palette.colors.error.700' },
|
|
512
|
+
default: { background: 'palette.colors.gray.100', foreground: 'palette.colors.gray.600' },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
475
515
|
scrollToBottom: {
|
|
476
516
|
background: 'components.button.primary.background',
|
|
477
517
|
foreground: 'components.button.primary.foreground',
|
|
@@ -1085,6 +1125,25 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
|
|
|
1085
1125
|
['scroll-to-bottom-gap', 'components-scrollToBottom-gap', '=0.5rem'],
|
|
1086
1126
|
['scroll-to-bottom-font-size', 'components-scrollToBottom-fontSize', 'palette-typography-fontSize-sm', '=0.875rem'],
|
|
1087
1127
|
['scroll-to-bottom-icon-size', 'components-scrollToBottom-iconSize', '=14px'],
|
|
1128
|
+
// Event-stream badge chips. Materialized in every theme (light defaults in
|
|
1129
|
+
// DEFAULT_COMPONENTS, dark pairs in the built-in dark layer), so the
|
|
1130
|
+
// chip's inline var() refs always resolve inside a themed root and
|
|
1131
|
+
// re-resolve on scheme toggles; the literal terminals only cover unthemed
|
|
1132
|
+
// direct mounts.
|
|
1133
|
+
['event-badge-flow-bg', 'components-eventStream-badge-flow-background', '=#dcfce7'],
|
|
1134
|
+
['event-badge-flow-fg', 'components-eventStream-badge-flow-foreground', '=#15803d'],
|
|
1135
|
+
['event-badge-step-bg', 'components-eventStream-badge-step-background', '=#f5f5f5'],
|
|
1136
|
+
['event-badge-step-fg', 'components-eventStream-badge-step-foreground', '=#0a0a0a'],
|
|
1137
|
+
['event-badge-reasoning-bg', 'components-eventStream-badge-reasoning-background', '=#fef9c3'],
|
|
1138
|
+
['event-badge-reasoning-fg', 'components-eventStream-badge-reasoning-foreground', '=#a16207'],
|
|
1139
|
+
['event-badge-tool-bg', 'components-eventStream-badge-tool-background', '=#f3e8ff'],
|
|
1140
|
+
['event-badge-tool-fg', 'components-eventStream-badge-tool-foreground', '=#6b21a8'],
|
|
1141
|
+
['event-badge-agent-bg', 'components-eventStream-badge-agent-background', '=#ccfbf1'],
|
|
1142
|
+
['event-badge-agent-fg', 'components-eventStream-badge-agent-foreground', '=#115e59'],
|
|
1143
|
+
['event-badge-error-bg', 'components-eventStream-badge-error-background', '=#fee2e2'],
|
|
1144
|
+
['event-badge-error-fg', 'components-eventStream-badge-error-foreground', '=#b91c1c'],
|
|
1145
|
+
['event-badge-default-bg', 'components-eventStream-badge-default-background', '=#f3f4f6'],
|
|
1146
|
+
['event-badge-default-fg', 'components-eventStream-badge-default-foreground', '=#4b5563'],
|
|
1088
1147
|
['tool-bubble-shadow', 'components-toolBubble-shadow', '=0 5px 15px rgba(15, 23, 42, 0.08)'],
|
|
1089
1148
|
['reasoning-bubble-shadow', 'components-reasoningBubble-shadow', '=0 5px 15px rgba(15, 23, 42, 0.08)'],
|
|
1090
1149
|
['composer-shadow', 'components-composer-shadow', '=none'],
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import type { VoiceConfig } from "../types";
|
|
1
|
+
import type { AgentWidgetConfig, VoiceConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Changes that require replacing a session's capture and playback provider. */
|
|
4
|
+
export function voiceConnectionChanged(previous: AgentWidgetConfig, next: AgentWidgetConfig): boolean {
|
|
5
|
+
const before = previous.voiceRecognition;
|
|
6
|
+
const after = next.voiceRecognition;
|
|
7
|
+
return (before?.enabled === true) !== (after?.enabled === true) ||
|
|
8
|
+
before?.provider?.type !== after?.provider?.type ||
|
|
9
|
+
before?.provider?.custom !== after?.provider?.custom ||
|
|
10
|
+
before?.provider?.runtype?.createPlaybackEngine !== after?.provider?.runtype?.createPlaybackEngine ||
|
|
11
|
+
JSON.stringify(before?.provider?.runtype) !== JSON.stringify(after?.provider?.runtype) ||
|
|
12
|
+
previous.agentId !== next.agentId || previous.clientToken !== next.clientToken || previous.apiUrl !== next.apiUrl;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Providers whose microphone lifecycle is owned by ChatSession instead of browser dictation. */
|
|
16
|
+
export function usesSessionVoice(config?: Partial<VoiceConfig>): boolean {
|
|
17
|
+
return config?.type === "runtype" || config?.type === "custom";
|
|
18
|
+
}
|
|
2
19
|
|
|
3
20
|
/**
|
|
4
21
|
* Eager, construction-free replica of the voice factory's `isVoiceSupported`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Correlate provider transcripts across cancellation and overlapping turns. */
|
|
2
|
+
export class VoiceTurnTracker {
|
|
3
|
+
private turnId?: string;
|
|
4
|
+
private cancelled = false;
|
|
5
|
+
|
|
6
|
+
start(turnId?: string): void {
|
|
7
|
+
this.turnId = turnId;
|
|
8
|
+
this.cancelled = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
cancel(): void {
|
|
12
|
+
this.cancelled = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
accepts(turnId?: string): boolean {
|
|
16
|
+
return !this.cancelled && (!turnId || !this.turnId || turnId === this.turnId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -11,7 +11,8 @@ import type {
|
|
|
11
11
|
// calls `initializeWebMCPPolyfill()` (idempotent install of document.modelContext).
|
|
12
12
|
// We stub the install as a no-op and provide `document.modelContext` ourselves,
|
|
13
13
|
// modeling the strict producer-preview surface the bridge consumes:
|
|
14
|
-
// - getTools(): async; inputSchema
|
|
14
|
+
// - getTools(): async; inputSchema in either generation (see
|
|
15
|
+
// `registry.schemaShape`); annotations only when the tool declared them
|
|
15
16
|
// - executeTool(info, argsJson, { signal }): async; validates+runs execute(),
|
|
16
17
|
// returns JSON.stringify(rawResult) or null; honors the abort signal.
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -45,21 +46,46 @@ type MockTool = {
|
|
|
45
46
|
description?: string;
|
|
46
47
|
inputSchema?: object;
|
|
47
48
|
title?: string;
|
|
49
|
+
annotations?: Record<string, unknown>;
|
|
48
50
|
execute: (args: Record<string, unknown>, client: MockClient) => unknown;
|
|
49
51
|
};
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Which generation of the WebMCP `inputSchema` contract the fake emits.
|
|
55
|
+
*
|
|
56
|
+
* `"object"` is what `@mcp-b/webmcp-polyfill` v5 and Chrome 154+ return after
|
|
57
|
+
* `webmcp#241`; `"string"` is the serialized form Chrome 149-153 and 154's
|
|
58
|
+
* same-document tools still emit. Both are live in the field, so the snapshot
|
|
59
|
+
* tests run against both -- pinning this fake to one generation is exactly how
|
|
60
|
+
* the v5 bump landed green while every real dispatch 400'd.
|
|
61
|
+
*/
|
|
62
|
+
type SchemaShape = "object" | "string";
|
|
63
|
+
|
|
64
|
+
const registry: { tools: MockTool[]; schemaShape: SchemaShape } = {
|
|
65
|
+
tools: [],
|
|
66
|
+
schemaShape: "object",
|
|
67
|
+
};
|
|
52
68
|
|
|
53
69
|
/** A fake `document.modelContext` exposing the strict consumer surface. */
|
|
54
70
|
const makeModelContext = () => ({
|
|
55
71
|
async getTools() {
|
|
56
|
-
// Mirrors the real polyfill's
|
|
57
|
-
//
|
|
72
|
+
// Mirrors the real polyfill's getTools(): `title` is always present, ""
|
|
73
|
+
// when the tool didn't declare one; annotations are absent. v5 OMITS
|
|
74
|
+
// `inputSchema` for a tool that registered none, where v4 always emitted a
|
|
75
|
+
// `{"type":"object"}` string.
|
|
58
76
|
return registry.tools.map((t) => ({
|
|
59
77
|
name: t.name,
|
|
60
78
|
description: t.description ?? `mock ${t.name}`,
|
|
61
|
-
|
|
79
|
+
...(t.inputSchema === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: {
|
|
82
|
+
inputSchema:
|
|
83
|
+
registry.schemaShape === "object"
|
|
84
|
+
? t.inputSchema
|
|
85
|
+
: JSON.stringify(t.inputSchema),
|
|
86
|
+
}),
|
|
62
87
|
title: t.title ?? "",
|
|
88
|
+
...(t.annotations === undefined ? {} : { annotations: t.annotations }),
|
|
63
89
|
}));
|
|
64
90
|
},
|
|
65
91
|
async executeTool(
|
|
@@ -110,6 +136,7 @@ beforeEach(() => {
|
|
|
110
136
|
vi.clearAllMocks();
|
|
111
137
|
polyfillMock.initThrows = false;
|
|
112
138
|
registry.tools = [];
|
|
139
|
+
registry.schemaShape = "object";
|
|
113
140
|
// location is read by snapshotForDispatch; document.modelContext is the
|
|
114
141
|
// consumer surface. Both are absent in the Node test environment.
|
|
115
142
|
vi.stubGlobal("location", { origin: "https://example.test" });
|
|
@@ -166,29 +193,111 @@ describe("WebMcpBridge.snapshotForDispatch", () => {
|
|
|
166
193
|
expect(bridge.isOperational()).toBe(false);
|
|
167
194
|
});
|
|
168
195
|
|
|
169
|
-
|
|
196
|
+
// Both `inputSchema` generations must produce the same wire tool: the object
|
|
197
|
+
// form (webmcp#241 / polyfill v5 / Chrome 154+) and the serialized string
|
|
198
|
+
// Chrome 149-153 still emits.
|
|
199
|
+
it.each(["object", "string"] as const)(
|
|
200
|
+
"ships only the JSON-serializable surface (name, description, schema, origin) [%s inputSchema]",
|
|
201
|
+
async (schemaShape) => {
|
|
202
|
+
registry.schemaShape = schemaShape;
|
|
203
|
+
registry.tools = [
|
|
204
|
+
fakeTool({
|
|
205
|
+
name: "search",
|
|
206
|
+
description: "search the shop",
|
|
207
|
+
inputSchema: { type: "object", properties: { q: { type: "string" } } },
|
|
208
|
+
}),
|
|
209
|
+
];
|
|
210
|
+
const bridge = new WebMcpBridge({ enabled: true });
|
|
211
|
+
const snap = await bridge.snapshotForDispatch();
|
|
212
|
+
expect(snap).toHaveLength(1);
|
|
213
|
+
const tool = snap[0]!;
|
|
214
|
+
expect(tool.name).toBe("search");
|
|
215
|
+
expect(tool.description).toBe("search the shop");
|
|
216
|
+
expect(tool.parametersSchema).toEqual({
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: { q: { type: "string" } },
|
|
219
|
+
});
|
|
220
|
+
expect(tool.origin).toBe("webmcp");
|
|
221
|
+
expect(tool.pageOrigin).toBe("https://example.test");
|
|
222
|
+
expect((tool as unknown as { execute?: unknown }).execute).toBeUndefined();
|
|
223
|
+
// Now that the registry was read, the bridge reports operational.
|
|
224
|
+
expect(bridge.isOperational()).toBe(true);
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// `parametersSchema` is required by the server's InlineClientToolSchema, and
|
|
229
|
+
// one entry missing it fails the whole DispatchRequestSchema union: a tool
|
|
230
|
+
// with no usable schema must degrade to "takes no arguments", never to an
|
|
231
|
+
// omitted key that 400s the turn.
|
|
232
|
+
it("always sends a parametersSchema, even when the tool declared none", async () => {
|
|
233
|
+
registry.tools = [fakeTool({ name: "ping" })];
|
|
234
|
+
const snap = await new WebMcpBridge({ enabled: true }).snapshotForDispatch();
|
|
235
|
+
expect(snap[0]!.parametersSchema).toEqual({ type: "object", properties: {} });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("forwards annotations and hoists untrustedContentHint to the top level", async () => {
|
|
170
239
|
registry.tools = [
|
|
171
240
|
fakeTool({
|
|
172
|
-
name: "
|
|
173
|
-
|
|
174
|
-
inputSchema: { type: "object", properties: { q: { type: "string" } } },
|
|
241
|
+
name: "get_reviews",
|
|
242
|
+
annotations: { readOnlyHint: true, untrustedContentHint: true },
|
|
175
243
|
}),
|
|
176
244
|
];
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
expect(tool.name).toBe("search");
|
|
182
|
-
expect(tool.description).toBe("search the shop");
|
|
183
|
-
expect(tool.parametersSchema).toEqual({
|
|
184
|
-
type: "object",
|
|
185
|
-
properties: { q: { type: "string" } },
|
|
245
|
+
const snap = await new WebMcpBridge({ enabled: true }).snapshotForDispatch();
|
|
246
|
+
expect(snap[0]!.annotations).toEqual({
|
|
247
|
+
readOnlyHint: true,
|
|
248
|
+
untrustedContentHint: true,
|
|
186
249
|
});
|
|
187
|
-
|
|
188
|
-
expect(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
250
|
+
// The server acts on the top-level field, not the nested metadata copy.
|
|
251
|
+
expect(snap[0]!.untrustedContentHint).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("omits annotations and the top-level hint when the tool declared none", async () => {
|
|
255
|
+
registry.tools = [fakeTool({ name: "search" })];
|
|
256
|
+
const snap = await new WebMcpBridge({ enabled: true }).snapshotForDispatch();
|
|
257
|
+
expect(snap[0]!).not.toHaveProperty("annotations");
|
|
258
|
+
expect(snap[0]!).not.toHaveProperty("untrustedContentHint");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("does not hoist untrustedContentHint: false, and drops unknown annotation keys", async () => {
|
|
262
|
+
registry.tools = [
|
|
263
|
+
fakeTool({
|
|
264
|
+
name: "add_to_cart",
|
|
265
|
+
annotations: {
|
|
266
|
+
readOnlyHint: false,
|
|
267
|
+
untrustedContentHint: false,
|
|
268
|
+
destructiveHint: true, // MCP-only today; not in the WebMCP dict
|
|
269
|
+
evil: () => "not JSON",
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
];
|
|
273
|
+
const snap = await new WebMcpBridge({ enabled: true }).snapshotForDispatch();
|
|
274
|
+
expect(snap[0]!.annotations).toEqual({
|
|
275
|
+
readOnlyHint: false,
|
|
276
|
+
untrustedContentHint: false,
|
|
277
|
+
});
|
|
278
|
+
expect(snap[0]!).not.toHaveProperty("untrustedContentHint");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it.each([
|
|
282
|
+
["unparseable string", "{not json"],
|
|
283
|
+
["non-object JSON string", '"just a string"'],
|
|
284
|
+
["JSON array string", "[1,2,3]"],
|
|
285
|
+
["array object", [1, 2, 3]],
|
|
286
|
+
["null", null],
|
|
287
|
+
])("falls back to an empty schema for %s", async (_label, inputSchema) => {
|
|
288
|
+
registry.tools = [fakeTool({ name: "odd" })];
|
|
289
|
+
// Bypass the fake's shape switch: these are malformed values a page or a
|
|
290
|
+
// future runtime could hand us, not either sanctioned generation.
|
|
291
|
+
vi.stubGlobal("document", {
|
|
292
|
+
modelContext: {
|
|
293
|
+
...makeModelContext(),
|
|
294
|
+
async getTools() {
|
|
295
|
+
return [{ name: "odd", description: "odd", inputSchema, title: "" }];
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
const snap = await new WebMcpBridge({ enabled: true }).snapshotForDispatch();
|
|
300
|
+
expect(snap[0]!.parametersSchema).toEqual({ type: "object", properties: {} });
|
|
192
301
|
});
|
|
193
302
|
|
|
194
303
|
it("applies client-side allowlist glob (`search_*`)", async () => {
|
package/src/webmcp-bridge.ts
CHANGED
|
@@ -49,8 +49,22 @@ export const WEBMCP_TOOL_PREFIX = "webmcp:";
|
|
|
49
49
|
export interface ModelContextToolInfo {
|
|
50
50
|
name: string;
|
|
51
51
|
description: string;
|
|
52
|
-
/**
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* JSON Schema for the tool's input, in EITHER generation of the WebMCP
|
|
54
|
+
* contract. Never read it raw: `normalizeInputSchema` in
|
|
55
|
+
* `webmcp-runtime-entry.ts` collapses both shapes to the object the wire
|
|
56
|
+
* needs.
|
|
57
|
+
*
|
|
58
|
+
* `webmcp#241` changed this from a JSON-encoded string to a JSON Schema
|
|
59
|
+
* object. `@mcp-b/webmcp-polyfill` made the switch in v5, and Chrome ships it
|
|
60
|
+
* from 154.0.8013 (cross-document tools first) -- but Chrome 149-153, most of
|
|
61
|
+
* the Origin Trial population, and 154's same-document tools still emit the
|
|
62
|
+
* string. Both are live in the field, so we accept both.
|
|
63
|
+
*
|
|
64
|
+
* Absent when the tool registered no schema: v5 omits the member, where v4
|
|
65
|
+
* always emitted a `{"type":"object"}` string.
|
|
66
|
+
*/
|
|
67
|
+
inputSchema?: object | string;
|
|
54
68
|
/**
|
|
55
69
|
* Display title declared on the tool (`ToolDescriptor.title` in the WebMCP
|
|
56
70
|
* spec). The polyfill returns `""` when the tool didn't declare one. Note:
|
|
@@ -59,6 +73,16 @@ export interface ModelContextToolInfo {
|
|
|
59
73
|
* channel available to us.
|
|
60
74
|
*/
|
|
61
75
|
title?: string;
|
|
76
|
+
/**
|
|
77
|
+
* WebMCP `Tool.annotations`. Returned by `getTools()` since
|
|
78
|
+
* `@mcp-b/webmcp-polyfill` v5 (and by Chrome's native implementation);
|
|
79
|
+
* absent on v4 and whenever the tool declared none -- the spec omits the
|
|
80
|
+
* member rather than emitting an empty object.
|
|
81
|
+
*/
|
|
82
|
+
annotations?: {
|
|
83
|
+
readOnlyHint?: boolean;
|
|
84
|
+
untrustedContentHint?: boolean;
|
|
85
|
+
};
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
export interface ModelContextCoreLike {
|
|
@@ -208,7 +232,7 @@ export const stripWebMcpPrefix = (name: string): string =>
|
|
|
208
232
|
|
|
209
233
|
/**
|
|
210
234
|
* `true` when `wireToolName` carries the `webmcp:` prefix. Used by `client.ts`
|
|
211
|
-
* to route `
|
|
235
|
+
* to route `await` events.
|
|
212
236
|
*/
|
|
213
237
|
export const isWebMcpToolName = (name: string): boolean =>
|
|
214
238
|
name.startsWith(WEBMCP_TOOL_PREFIX);
|