@runtypelabs/persona 4.33.0 → 4.35.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/activity-ui.d.cts +3 -15
- package/dist/activity-ui.d.ts +3 -15
- package/dist/animations/glyph-cycle.d.cts +1 -3
- package/dist/animations/glyph-cycle.d.ts +1 -3
- package/dist/animations/wipe.d.cts +1 -3
- package/dist/animations/wipe.d.ts +1 -3
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +3 -15
- package/dist/approval-ui.d.ts +3 -15
- package/dist/approval-ui.js +1 -1
- package/dist/artifacts-ui.cjs +1 -1
- package/dist/artifacts-ui.d.cts +3 -21
- package/dist/artifacts-ui.d.ts +3 -21
- package/dist/background-runtime.d.cts +2 -3
- package/dist/background-runtime.d.ts +2 -3
- package/dist/{chunk-DW7Vg_l2.js → chunk-B1P8w9_X.js} +1 -1
- package/dist/{chunk-D6U0E82p.js → chunk-B30Jdgg6.js} +1 -1
- package/dist/{chunk-gu34lr-P.js → chunk-Brdx4NCz.js} +1 -1
- package/dist/{chunk-6CUrH_jm.js → chunk-Bzo9GaAi.js} +1 -1
- package/dist/{chunk-Ds_twaZn.js → chunk-C4v-HNhS.js} +1 -1
- package/dist/{chunk-Bkma5hIU.js → chunk-CE4seJxF.js} +1 -1
- package/dist/chunk-CNBMBx-Y.js +2 -0
- package/dist/{chunk-ql5N2I_t.js → chunk-CWzHNuVK.js} +1 -1
- package/dist/{chunk-DbGLjU2P.js → chunk-CYpt7PsO.js} +1 -1
- package/dist/chunk-Cd8mp8Sv.js +1 -0
- package/dist/chunk-CsfE8WRG.js +1 -0
- package/dist/{chunk-DOZmcve8.js → chunk-Csr59G5V.js} +1 -1
- package/dist/{chunk-Co3VVwGe.js → chunk-D2pHRNEq.js} +0 -0
- package/dist/chunk-D3kZO8u-.js +1 -0
- package/dist/chunk-DAthf5H3.js +1 -0
- package/dist/{chunk-CXQJG7pb.js → chunk-DZ_aillY.js} +2 -2
- package/dist/{chunk-6C23QQdP.js → chunk-DgJV9zbK.js} +1 -1
- package/dist/chunk-Dv6KtIT1.js +1 -0
- package/dist/chunk-Dws8IrdR.js +1 -0
- package/dist/chunk-ThxxD8oK.js +1 -0
- package/dist/{chunk-C-EHtpbB.js → chunk-h5aOI_Pa.js} +1 -1
- package/dist/chunk-kizhspe5.js +1 -0
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/{code-generators-DqauYyfX.d.cts → code-generators-DPAqTcAz.d.cts} +0 -2
- package/dist/{code-generators-DqauYyfX.d.ts → code-generators-DPAqTcAz.d.ts} +0 -2
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +4 -20
- package/dist/composer-actions-runtime.d.ts +4 -20
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-DnqGCXZz.d.ts → composer-input-BSDNPOqV.d.cts} +1 -7
- package/dist/{composer-input-DnqGCXZz.d.cts → composer-input-BSDNPOqV.d.ts} +1 -7
- package/dist/{composer-store-6Iwy6WLI.d.ts → composer-store-DC-gAjtN.d.cts} +1 -3
- package/dist/{composer-store-6Iwy6WLI.d.cts → composer-store-DC-gAjtN.d.ts} +1 -3
- package/dist/config-diagnostics.d.cts +0 -2
- package/dist/config-diagnostics.d.ts +0 -2
- package/dist/{context-mention-manager-B-rHF98n.d.ts → context-mention-manager-CCpeoGL2.d.cts} +2 -6
- package/dist/{context-mention-manager-B-rHF98n.d.cts → context-mention-manager-CCpeoGL2.d.ts} +2 -6
- package/dist/context-mentions-inline.cjs +1 -1
- package/dist/context-mentions-inline.d.cts +2 -4
- package/dist/context-mentions-inline.d.ts +2 -4
- package/dist/context-mentions-inline.js +1 -1
- package/dist/context-mentions.cjs +2 -2
- package/dist/context-mentions.d.cts +3 -9
- package/dist/context-mentions.d.ts +3 -9
- package/dist/context-mentions.js +1 -1
- package/dist/{defaults-BuOjqUAG.d.ts → defaults-tj5RcN8r.d.cts} +1 -5
- package/dist/{defaults-BuOjqUAG.d.cts → defaults-tj5RcN8r.d.ts} +1 -5
- package/dist/dictation-runtime.cjs +1 -1
- package/dist/dictation-runtime.d.cts +4 -13
- package/dist/dictation-runtime.d.ts +4 -13
- package/dist/dictation-runtime.js +1 -1
- package/dist/{dom-context-DHTabIn7.d.cts → dom-context-Bw-_9Evh.d.cts} +0 -2
- package/dist/{dom-context-DHTabIn7.d.ts → dom-context-Bw-_9Evh.d.ts} +0 -2
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +3 -16
- package/dist/event-stream-view.d.ts +3 -16
- package/dist/{fallback-speech-engine-SqEfkGda.d.ts → fallback-speech-engine-gkhbjLiZ.d.cts} +1 -3
- package/dist/{fallback-speech-engine-SqEfkGda.d.cts → fallback-speech-engine-gkhbjLiZ.d.ts} +1 -3
- package/dist/{feature-preferences-DqAmje-2.d.ts → feature-preferences-CerDWGZ7.d.cts} +1 -5
- package/dist/{feature-preferences-DqAmje-2.d.cts → feature-preferences-CerDWGZ7.d.ts} +1 -5
- package/dist/forms-ui.d.cts +2 -4
- package/dist/forms-ui.d.ts +2 -4
- package/dist/{history-confirm-dialog-B3L7QNcO.d.cts → history-confirm-dialog-CrkSB3Ra.d.cts} +2 -14
- package/dist/{history-confirm-dialog-B3L7QNcO.d.ts → history-confirm-dialog-CrkSB3Ra.d.ts} +2 -14
- package/dist/history-shell.cjs +1 -1
- package/dist/history-shell.d.cts +8 -27
- package/dist/history-shell.d.ts +8 -27
- package/dist/history-shell.js +1 -1
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/icons-extra.cjs +1 -1
- package/dist/icons-extra.d.cts +0 -2
- package/dist/icons-extra.d.ts +0 -2
- package/dist/index.cjs +44 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +50 -50
- 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/{loading-label-runtime-loader-JecjOfJS.d.ts → loading-label-runtime-loader-fWIRrhUF.d.cts} +1 -3
- package/dist/{loading-label-runtime-loader-JecjOfJS.d.cts → loading-label-runtime-loader-fWIRrhUF.d.ts} +1 -3
- package/dist/loading-label-runtime.cjs +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/loading-label-runtime.js +1 -1
- package/dist/markdown-parsers.js +1 -1
- package/dist/{message-bubble-CBYtpDsT.d.ts → message-bubble-Czc0lOHr.d.cts} +20 -3
- package/dist/{message-bubble-CBYtpDsT.d.cts → message-bubble-Czc0lOHr.d.ts} +20 -3
- package/dist/{plugin-kit-C1mRjLgW.d.cts → plugin-kit-C30jE-cw.d.cts} +3 -3
- package/dist/{plugin-kit-C1mRjLgW.d.ts → plugin-kit-C30jE-cw.d.ts} +3 -3
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/preferences-runtime.cjs +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/preferences-runtime.js +1 -1
- package/dist/{preview-_UJwwt2L.d.cts → preview-D-osV3FU.d.cts} +2 -6
- package/dist/{preview-_UJwwt2L.d.ts → preview-D-osV3FU.d.ts} +2 -6
- package/dist/{registry-B5ZV3K-h.d.ts → registry-B6w91tx0.d.cts} +1 -8
- package/dist/{registry-B5ZV3K-h.d.cts → registry-B6w91tx0.d.ts} +1 -8
- package/dist/{session-Bys8_XuB.d.cts → session-BUQrs3Of.d.cts} +10 -93
- package/dist/{session-Bys8_XuB.d.ts → session-BUQrs3Of.d.ts} +10 -93
- package/dist/session-reconnect.d.cts +2 -4
- package/dist/session-reconnect.d.ts +2 -4
- package/dist/smart-dom-reader.d.cts +2 -8
- package/dist/smart-dom-reader.d.ts +2 -8
- package/dist/testing.d.cts +0 -2
- package/dist/testing.d.ts +0 -2
- package/dist/theme-editor-preview.cjs +50 -50
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +25 -25
- package/dist/theme-editor.cjs +25 -5
- package/dist/theme-editor.d.cts +2 -22
- package/dist/theme-editor.d.ts +2 -22
- package/dist/theme-editor.js +1 -1
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -4
- package/dist/theme-reference.d.ts +2 -4
- package/dist/theme-reference.js +1 -1
- package/dist/{tool-loading-animation-d5qAQcwv.d.ts → tool-loading-animation-B7XUf8TN.d.cts} +1 -7
- package/dist/{tool-loading-animation-d5qAQcwv.d.cts → tool-loading-animation-B7XUf8TN.d.ts} +1 -7
- package/dist/{tooltip-BkW-8MnD.d.cts → tooltip-DbXkAuJL.d.cts} +0 -2
- package/dist/{tooltip-BkW-8MnD.d.ts → tooltip-DbXkAuJL.d.ts} +0 -2
- package/dist/{turn-activity-ui-q6fHVnx-.d.cts → turn-activity-ui-CohuNMHT.d.cts} +2 -12
- package/dist/{turn-activity-ui-q6fHVnx-.d.ts → turn-activity-ui-CohuNMHT.d.ts} +2 -12
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-B_mj21jg.d.cts → types-9i0xcAMT.d.cts} +128 -29
- package/dist/{types-B_mj21jg.d.ts → types-9i0xcAMT.d.ts} +128 -29
- package/dist/ui-extras.js +3 -3
- package/dist/voice-client-tools.d.cts +1 -3
- package/dist/voice-client-tools.d.ts +1 -3
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -12
- package/dist/voice-runtime.d.ts +2 -12
- package/dist/voice-runtime.js +2 -2
- package/dist/voice-worklet-player.d.cts +2 -6
- package/dist/voice-worklet-player.d.ts +2 -6
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.ts → webmcp-runtime-entry-XNaXa-9h.d.cts} +9 -5
- package/dist/{webmcp-runtime-entry-ClEO3qdI.d.cts → webmcp-runtime-entry-XNaXa-9h.d.ts} +9 -5
- package/dist/webmcp-runtime.cjs +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/webmcp-runtime.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +5 -5
- package/src/client-dispatch.ts +13 -0
- package/src/client.ts +37 -2
- package/src/client.webmcp-require-confirm.test.ts +212 -0
- package/src/components/composer-model-picker.ts +0 -14
- package/src/components/composer-overflow-menu.ts +0 -15
- package/src/components/composer-tokens.ts +0 -20
- package/src/components/event-stream-view.test.ts +5 -0
- package/src/components/message-bubble.ts +26 -6
- package/src/components/scroll-to-bottom.test.ts +135 -0
- package/src/components/scroll-to-bottom.ts +50 -13
- package/src/components/user-message-collapse.ts +117 -0
- package/src/defaults.ts +6 -0
- package/src/generated/runtype-openapi-contract.ts +13 -0
- package/src/plugin-kit.test.ts +101 -0
- package/src/plugin-kit.ts +29 -1
- package/src/session.ts +6 -2
- package/src/session.webmcp.test.ts +32 -0
- package/src/styles/widget.css +56 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +29 -0
- package/src/types.ts +84 -5
- package/src/ui.composer-models.test.ts +5 -7
- package/src/ui.composer-overflow.test.ts +14 -21
- package/src/ui.scroll-additive.test.ts +219 -117
- package/src/ui.scroll.test.ts +176 -13
- package/src/ui.ts +230 -107
- package/src/ui.turn-activity.test.ts +7 -7
- package/src/ui.user-message-collapse.test.ts +540 -0
- package/src/utils/auto-follow.test.ts +0 -38
- package/src/utils/auto-follow.ts +0 -35
- package/src/utils/context-mention-controller.test.ts +56 -0
- package/src/utils/theme.test.ts +23 -0
- package/src/utils/tokens.ts +16 -0
- package/src/webmcp-bridge.test.ts +102 -0
- package/src/webmcp-runtime-entry.ts +29 -1
- package/dist/chunk-BAkaTZpd.js +0 -1
- package/dist/chunk-BEDTp-dm.js +0 -1
- package/dist/chunk-C7-m0EmI.js +0 -1
- package/dist/chunk-CNhThrLX.js +0 -1
- package/dist/chunk-CVMIncEY.js +0 -1
- package/dist/chunk-CzLEhoB_.js +0 -2
- package/dist/chunk-DVgzoVCB.js +0 -1
- package/dist/chunk-d54BJdKd.js +0 -1
- package/dist/chunk-dLJWKfOb.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.35.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",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"@types/jsdom": "^30.0.0",
|
|
192
192
|
"@types/node": "^20.12.7",
|
|
193
193
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
194
|
-
"@vitest/ui": "^5.0.
|
|
194
|
+
"@vitest/ui": "^5.0.5",
|
|
195
195
|
"acorn": "^8.15.0",
|
|
196
196
|
"fake-indexeddb": "^6.2.5",
|
|
197
197
|
"jsdom": "^28.1.0",
|
|
@@ -199,9 +199,9 @@
|
|
|
199
199
|
"rimraf": "^5.0.5",
|
|
200
200
|
"size-limit": "^12.1.0",
|
|
201
201
|
"terser": "^5.49.0",
|
|
202
|
-
"tsdown": "^0.23.
|
|
202
|
+
"tsdown": "^0.23.2",
|
|
203
203
|
"typescript": "^6.0.3",
|
|
204
|
-
"vitest": "^5.0.
|
|
204
|
+
"vitest": "^5.0.5",
|
|
205
205
|
"webmcp-polyfill-v6": "npm:@mcp-b/webmcp-polyfill@6.0.0"
|
|
206
206
|
},
|
|
207
207
|
"engines": {
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"check:size": "node scripts/check-size.mjs",
|
|
236
236
|
"check:dist-vite8": "node scripts/check-dist-no-in-for-init.mjs",
|
|
237
237
|
"build:styles": "node scripts/build-styles.mjs",
|
|
238
|
-
"lint": "
|
|
238
|
+
"lint": "node ../../scripts/lint.mjs",
|
|
239
239
|
"typecheck": "pnpm run check:runtype-types && node ../../scripts/typecheck.mjs --noEmit",
|
|
240
240
|
"test": "vitest",
|
|
241
241
|
"test:ui": "vitest --ui",
|
package/src/client-dispatch.ts
CHANGED
|
@@ -394,6 +394,8 @@ export class ClientDispatch {
|
|
|
394
394
|
welcomeMessage: data.config.welcomeMessage,
|
|
395
395
|
placeholder: data.config.placeholder,
|
|
396
396
|
theme: data.config.theme as ClientSession["config"]["theme"],
|
|
397
|
+
// Kept only as a non-empty array; the bridge ignores non-string entries.
|
|
398
|
+
...readWebMcpInitConfig(data.config),
|
|
397
399
|
},
|
|
398
400
|
};
|
|
399
401
|
}
|
|
@@ -1087,3 +1089,14 @@ export class ClientDispatch {
|
|
|
1087
1089
|
}
|
|
1088
1090
|
|
|
1089
1091
|
}
|
|
1092
|
+
|
|
1093
|
+
/** `config.webmcp` from `/v1/client/init`, kept only with a non-empty pattern array. */
|
|
1094
|
+
const readWebMcpInitConfig = (
|
|
1095
|
+
config: unknown
|
|
1096
|
+
): Pick<ClientSession["config"], "webmcp"> => {
|
|
1097
|
+
const list = (config as { webmcp?: { requireConfirmFor?: unknown } }).webmcp
|
|
1098
|
+
?.requireConfirmFor;
|
|
1099
|
+
return Array.isArray(list) && list.length
|
|
1100
|
+
? { webmcp: { requireConfirmFor: list as string[] } }
|
|
1101
|
+
: {};
|
|
1102
|
+
};
|
package/src/client.ts
CHANGED
|
@@ -181,7 +181,18 @@ export class AgentWidgetClient {
|
|
|
181
181
|
* matches is superseded, and every SSE frame it still receives is dropped
|
|
182
182
|
*/
|
|
183
183
|
#currentClientTurnId: string | null = null;
|
|
184
|
-
|
|
184
|
+
#clientSession: ClientSession | null = null;
|
|
185
|
+
/** Last surface `requireConfirmFor` installed with a session; `null` until one loads. */
|
|
186
|
+
#knownRequireConfirmFor: readonly string[] | null = null;
|
|
187
|
+
// Every install path assigns here, so the surface policy is recorded the
|
|
188
|
+
// moment a session lands, not when a tool call next reads it.
|
|
189
|
+
private get clientSession(): ClientSession | null {
|
|
190
|
+
return this.#clientSession;
|
|
191
|
+
}
|
|
192
|
+
private set clientSession(session: ClientSession | null) {
|
|
193
|
+
this.#clientSession = session;
|
|
194
|
+
if (session) this.#knownRequireConfirmFor = session.config?.webmcp?.requireConfirmFor ?? [];
|
|
195
|
+
}
|
|
185
196
|
#sessionInitPromise: Promise<ClientSession> | null = null;
|
|
186
197
|
/**
|
|
187
198
|
* Early-init latch for `warmSession()`. `false` = armed. Otherwise it holds
|
|
@@ -275,6 +286,7 @@ export class AgentWidgetClient {
|
|
|
275
286
|
recordToolDisplayTitles: recordWebMcpToolDisplayTitles,
|
|
276
287
|
getToolDisplayTitle: getWebMcpToolDisplayTitle,
|
|
277
288
|
loadPolyfill: loadWebMcpPolyfillModule,
|
|
289
|
+
getRequireConfirmFor: () => this.#surfaceRequireConfirmFor(),
|
|
278
290
|
});
|
|
279
291
|
if (this.#pendingWebMcpConfirmHandler !== undefined) {
|
|
280
292
|
bridge.setConfirmHandler(this.#pendingWebMcpConfirmHandler);
|
|
@@ -408,12 +420,35 @@ export class AgentWidgetClient {
|
|
|
408
420
|
if (!b) {
|
|
409
421
|
return { status: "failed", error: { message: "WebMCP runtime failed to load." } };
|
|
410
422
|
}
|
|
411
|
-
return b.executeVoiceToolCall(name, args, options);
|
|
423
|
+
return b.executeVoiceToolCall(name, args, await this.#voiceToolOptions(options));
|
|
412
424
|
},
|
|
413
425
|
};
|
|
414
426
|
return surface;
|
|
415
427
|
}
|
|
416
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The bound surface's `requireConfirmFor` for the WebMCP bridge. Sticky: the
|
|
431
|
+
* policy of the last installed session survives `clearClientSession()` (same
|
|
432
|
+
* token, same surface), so a session cleared mid-call cannot drop it. `null`
|
|
433
|
+
* means client-token mode with no policy loaded yet; the bridge then
|
|
434
|
+
* confirms every call. `undefined` outside client-token mode.
|
|
435
|
+
*/
|
|
436
|
+
#surfaceRequireConfirmFor(): readonly string[] | null | undefined {
|
|
437
|
+
return this.isClientTokenMode() ? this.#knownRequireConfirmFor : undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* A voice call can start before any typed turn has run client-token init.
|
|
442
|
+
* Try to load the surface policy first so read-only tools keep their fast
|
|
443
|
+
* tier; on failure the policy stays unknown and the bridge gates the call.
|
|
444
|
+
*/
|
|
445
|
+
async #voiceToolOptions<T extends { autoConfirmReadOnly: boolean }>(options: T): Promise<T> {
|
|
446
|
+
if (options.autoConfirmReadOnly && this.isClientTokenMode() && !this.clientSession) {
|
|
447
|
+
await this.initSession().catch(() => undefined);
|
|
448
|
+
}
|
|
449
|
+
return options;
|
|
450
|
+
}
|
|
451
|
+
|
|
417
452
|
public executeWebMcpToolCall(
|
|
418
453
|
wireToolName: string,
|
|
419
454
|
args: unknown,
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { AgentWidgetClient } from "./client";
|
|
6
|
+
import type { AgentWidgetConfig, ClientInitResponse, WebMcpConfirmHandler } from "./types";
|
|
7
|
+
|
|
8
|
+
const API_URL = "https://api.runtype.com";
|
|
9
|
+
|
|
10
|
+
const initBody = (config: Record<string, unknown> = {}): ClientInitResponse =>
|
|
11
|
+
({
|
|
12
|
+
sessionId: "sess_1",
|
|
13
|
+
expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
|
|
14
|
+
flow: { id: "flow_1", name: "Flow", description: null },
|
|
15
|
+
config: { welcomeMessage: null, placeholder: "Ask...", theme: null, ...config },
|
|
16
|
+
}) as ClientInitResponse;
|
|
17
|
+
|
|
18
|
+
const installInit = (body: ClientInitResponse) => {
|
|
19
|
+
global.fetch = vi.fn(async () => ({
|
|
20
|
+
ok: true,
|
|
21
|
+
status: 200,
|
|
22
|
+
json: async () => body,
|
|
23
|
+
})) as unknown as typeof fetch;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const makeClient = (webmcp?: AgentWidgetConfig["webmcp"]) =>
|
|
27
|
+
new AgentWidgetClient({
|
|
28
|
+
apiUrl: API_URL,
|
|
29
|
+
clientToken: "ct_live_x",
|
|
30
|
+
...(webmcp ? { webmcp } : {}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
delete (document as Document & { modelContext?: unknown }).modelContext;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("client-token init: config.webmcp.requireConfirmFor", () => {
|
|
39
|
+
it("keeps the surface's patterns on the session", async () => {
|
|
40
|
+
installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
|
|
41
|
+
const session = await makeClient().initSession();
|
|
42
|
+
expect(session.config.webmcp).toEqual({ requireConfirmFor: ["checkout_*"] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
["absent", {}],
|
|
47
|
+
["empty", { webmcp: { requireConfirmFor: [] } }],
|
|
48
|
+
["malformed", { webmcp: { requireConfirmFor: "checkout_*" } }],
|
|
49
|
+
])("omits config.webmcp when the list is %s", async (_label, config) => {
|
|
50
|
+
installInit(initBody(config));
|
|
51
|
+
const session = await makeClient().initSession();
|
|
52
|
+
expect(session.config).not.toHaveProperty("webmcp");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("client-token init feeds the WebMCP confirm gate", () => {
|
|
57
|
+
it("flags a matched page tool for the host's onConfirm after init", async () => {
|
|
58
|
+
const execute = vi.fn(async () => JSON.stringify("ran"));
|
|
59
|
+
(document as Document & { modelContext?: unknown }).modelContext = {
|
|
60
|
+
getTools: async () => [
|
|
61
|
+
{ name: "checkout_submit", description: "Place the order", title: "", window, origin: location.origin },
|
|
62
|
+
{ name: "search", description: "Search", title: "", window, origin: location.origin },
|
|
63
|
+
],
|
|
64
|
+
executeTool: execute,
|
|
65
|
+
};
|
|
66
|
+
installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
|
|
67
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
68
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
69
|
+
await client.initSession();
|
|
70
|
+
|
|
71
|
+
await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
|
|
72
|
+
await client.executeWebMcpToolCall("webmcp:search", {});
|
|
73
|
+
|
|
74
|
+
expect(onConfirm).toHaveBeenCalledTimes(2);
|
|
75
|
+
expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({
|
|
76
|
+
toolName: "checkout_submit",
|
|
77
|
+
confirmRequired: true,
|
|
78
|
+
});
|
|
79
|
+
expect(onConfirm.mock.calls[1]?.[0]).not.toHaveProperty("confirmRequired");
|
|
80
|
+
expect(execute).toHaveBeenCalledTimes(2);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("voice before any client-token init", () => {
|
|
85
|
+
const installReadOnlyTools = () => {
|
|
86
|
+
(document as Document & { modelContext?: unknown }).modelContext = {
|
|
87
|
+
getTools: async () =>
|
|
88
|
+
["checkout_preview", "next_slide"].map((name) => ({
|
|
89
|
+
name,
|
|
90
|
+
description: name,
|
|
91
|
+
title: "",
|
|
92
|
+
window,
|
|
93
|
+
origin: location.origin,
|
|
94
|
+
annotations: { readOnlyHint: true },
|
|
95
|
+
})),
|
|
96
|
+
executeTool: vi.fn(async () => JSON.stringify("ran")),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const voiceExecute = (client: AgentWidgetClient, name: string) =>
|
|
100
|
+
client
|
|
101
|
+
.getVoiceClientTools()!
|
|
102
|
+
.execute(name, {}, { signal: new AbortController().signal, autoConfirmReadOnly: true });
|
|
103
|
+
|
|
104
|
+
it("loads the surface policy so a matched read-only tool still confirms", async () => {
|
|
105
|
+
installReadOnlyTools();
|
|
106
|
+
installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
|
|
107
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
|
|
108
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
109
|
+
|
|
110
|
+
const matched = await voiceExecute(client, "webmcp:checkout_preview");
|
|
111
|
+
const unmatched = await voiceExecute(client, "webmcp:next_slide");
|
|
112
|
+
|
|
113
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({
|
|
116
|
+
toolName: "checkout_preview",
|
|
117
|
+
confirmRequired: true,
|
|
118
|
+
});
|
|
119
|
+
expect(matched.status).toBe("denied");
|
|
120
|
+
expect(unmatched.status).toBe("completed");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps every voice call gated when init fails", async () => {
|
|
124
|
+
installReadOnlyTools();
|
|
125
|
+
global.fetch = vi.fn(async () => {
|
|
126
|
+
throw new Error("offline");
|
|
127
|
+
}) as unknown as typeof fetch;
|
|
128
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
|
|
129
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
130
|
+
|
|
131
|
+
const result = await voiceExecute(client, "webmcp:next_slide");
|
|
132
|
+
|
|
133
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(result.status).toBe("denied");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("surface policy lifetime", () => {
|
|
139
|
+
const installTools = () => {
|
|
140
|
+
(document as Document & { modelContext?: unknown }).modelContext = {
|
|
141
|
+
getTools: async () =>
|
|
142
|
+
["checkout_submit", "search"].map((name) => ({
|
|
143
|
+
name,
|
|
144
|
+
description: name,
|
|
145
|
+
title: "",
|
|
146
|
+
window,
|
|
147
|
+
origin: location.origin,
|
|
148
|
+
})),
|
|
149
|
+
executeTool: vi.fn(async () => JSON.stringify("ran")),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
it("keeps the last loaded policy after clearClientSession()", async () => {
|
|
154
|
+
installTools();
|
|
155
|
+
installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
|
|
156
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
157
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
158
|
+
await client.initSession();
|
|
159
|
+
// Read the policy once while the session is live, then drop the session.
|
|
160
|
+
await client.executeWebMcpToolCall("webmcp:search", {});
|
|
161
|
+
client.clearClientSession();
|
|
162
|
+
|
|
163
|
+
await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
|
|
164
|
+
await client.executeWebMcpToolCall("webmcp:search", {});
|
|
165
|
+
|
|
166
|
+
expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([
|
|
167
|
+
undefined,
|
|
168
|
+
true,
|
|
169
|
+
undefined,
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("records a policy when a session installs, not when a call reads it", async () => {
|
|
174
|
+
installTools();
|
|
175
|
+
// First session: no policy. A tool call reads (and would have cached) [].
|
|
176
|
+
installInit(initBody());
|
|
177
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
178
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
179
|
+
await client.initSession();
|
|
180
|
+
await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
|
|
181
|
+
|
|
182
|
+
// Second session adds a policy, then is cleared before any tool call reads it.
|
|
183
|
+
client.clearClientSession();
|
|
184
|
+
installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
|
|
185
|
+
await client.initSession();
|
|
186
|
+
client.clearClientSession();
|
|
187
|
+
|
|
188
|
+
await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
|
|
189
|
+
|
|
190
|
+
expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([undefined, true]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("flags every call while no client-token policy has loaded", async () => {
|
|
194
|
+
installTools();
|
|
195
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
196
|
+
const client = makeClient({ enabled: true, onConfirm });
|
|
197
|
+
|
|
198
|
+
await client.executeWebMcpToolCall("webmcp:search", {});
|
|
199
|
+
|
|
200
|
+
expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({ toolName: "search", confirmRequired: true });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("leaves proxy mode (no client token) unflagged", async () => {
|
|
204
|
+
installTools();
|
|
205
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
206
|
+
const client = new AgentWidgetClient({ apiUrl: API_URL, webmcp: { enabled: true, onConfirm } });
|
|
207
|
+
|
|
208
|
+
await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
|
|
209
|
+
|
|
210
|
+
expect(onConfirm.mock.calls[0]?.[0]).not.toHaveProperty("confirmRequired");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -18,25 +18,12 @@ import { createPopover, type PopoverHandle } from "../plugin-kit";
|
|
|
18
18
|
import type { ComposerCustomAction, ComposerModel } from "../types";
|
|
19
19
|
import { createElement, createNode } from "../utils/dom";
|
|
20
20
|
import type { renderLucideIcon } from "../utils/icons";
|
|
21
|
-
import { forwardMenuTokens } from "./composer-tokens";
|
|
22
21
|
|
|
23
22
|
export const COMPOSER_MODEL_PICKER_ACTION_ID = "core:model";
|
|
24
23
|
export const COMPOSER_MODEL_PICKER_ORDER = 700;
|
|
25
24
|
|
|
26
25
|
const ROW_ICON_SIZE = 16;
|
|
27
26
|
|
|
28
|
-
/**
|
|
29
|
-
* Tokens the popover panel reads, forwarded onto the portaled panel on every
|
|
30
|
-
* open; unset ones clear so the stylesheet's fallbacks stand.
|
|
31
|
-
*/
|
|
32
|
-
const MENU_TOKEN_VARS = [
|
|
33
|
-
"--persona-components-composer-modelPicker-menuBackground",
|
|
34
|
-
"--persona-components-composer-modelPicker-menuBorderRadius",
|
|
35
|
-
"--persona-components-composer-modelPicker-rowHoverBackground",
|
|
36
|
-
"--persona-components-composer-modelPicker-labelColor",
|
|
37
|
-
"--persona-components-composer-modelPicker-descriptionColor",
|
|
38
|
-
] as const;
|
|
39
|
-
|
|
40
27
|
export interface ComposerModelPickerOptions {
|
|
41
28
|
renderIcon: typeof renderLucideIcon;
|
|
42
29
|
getModels: () => ReadonlyArray<ComposerModel>;
|
|
@@ -212,7 +199,6 @@ export function createComposerModelPickerAction(
|
|
|
212
199
|
onDismiss: () => trigger.setAttribute("aria-expanded", "false"),
|
|
213
200
|
});
|
|
214
201
|
}
|
|
215
|
-
forwardMenuTokens(trigger, menu, MENU_TOKEN_VARS);
|
|
216
202
|
popover.open();
|
|
217
203
|
trigger.setAttribute("aria-expanded", "true");
|
|
218
204
|
const ids = [...rows.keys()];
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
COMPOSER_CONTROL_CLASS,
|
|
23
23
|
COMPOSER_CONTROL_GLYPH_CLASS,
|
|
24
24
|
COMPOSER_CONTROL_ICON_FALLBACK_PX,
|
|
25
|
-
forwardMenuTokens,
|
|
26
25
|
} from "./composer-tokens";
|
|
27
26
|
|
|
28
27
|
export interface ComposerOverflowMenuOptions {
|
|
@@ -66,19 +65,6 @@ export interface ComposerOverflowMenu {
|
|
|
66
65
|
const focusableSelector =
|
|
67
66
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
68
67
|
|
|
69
|
-
/**
|
|
70
|
-
* Panel surface tokens, forwarded onto the portaled panel on every open; unset
|
|
71
|
-
* ones clear so the stylesheet's fallbacks stand.
|
|
72
|
-
*/
|
|
73
|
-
const MENU_TOKEN_VARS = [
|
|
74
|
-
"--persona-components-composer-overflowMenu-background",
|
|
75
|
-
"--persona-components-composer-overflowMenu-borderColor",
|
|
76
|
-
"--persona-components-composer-overflowMenu-borderRadius",
|
|
77
|
-
"--persona-components-composer-overflowMenu-foreground",
|
|
78
|
-
"--persona-components-composer-overflowMenu-shadow",
|
|
79
|
-
...["padding", "rowGap", "rowPadding", "rowBorderRadius", "fontSize", "lineHeight", "iconSize", "iconGap", "hoverBackground", "focusBackground", "descriptionColor", "descriptionFontSize", "dividerColor", "dividerSpacing", "minWidth", "maxHeight"].map((name) => `--persona-components-composer-overflowMenu-${name}`),
|
|
80
|
-
] as const;
|
|
81
|
-
|
|
82
68
|
/**
|
|
83
69
|
* Set on the menu row that holds keyboard focus. Rows that shade on
|
|
84
70
|
* `:focus-visible` do not need it; wrapper rows around a live control do.
|
|
@@ -296,7 +282,6 @@ export function createComposerOverflowMenu(
|
|
|
296
282
|
};
|
|
297
283
|
|
|
298
284
|
const refresh = (): void => {
|
|
299
|
-
forwardMenuTokens(triggerButton, panel, MENU_TOKEN_VARS);
|
|
300
285
|
panel.setAttribute("data-persona-menu-description-layout", options.getConfig?.()?.descriptionLayout ?? "stacked");
|
|
301
286
|
const width = options.getConfig?.()?.width;
|
|
302
287
|
panel.style.width = width === "composer"
|
|
@@ -12,23 +12,3 @@ export const COMPOSER_CONTROL_GLYPH_CLASS = "persona-composer-control--glyph";
|
|
|
12
12
|
export const COMPOSER_CONTROL_FALLBACK_PX = 40;
|
|
13
13
|
/** Icon-size token default in CSS pixels, for the same JS-derived cases. */
|
|
14
14
|
export const COMPOSER_CONTROL_ICON_FALLBACK_PX = 24;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Forward theme vars onto a portaled menu panel, which lives outside the
|
|
18
|
-
* themed mount. The panel is reused across opens, so an unset var must clear
|
|
19
|
-
* any previously stamped value for the stylesheet fallback to paint again.
|
|
20
|
-
*/
|
|
21
|
-
export const forwardMenuTokens = (
|
|
22
|
-
from: HTMLElement,
|
|
23
|
-
to: HTMLElement,
|
|
24
|
-
names: readonly string[]
|
|
25
|
-
): void => {
|
|
26
|
-
const view = from.ownerDocument?.defaultView;
|
|
27
|
-
if (!view) return;
|
|
28
|
-
const computed = view.getComputedStyle(from);
|
|
29
|
-
for (const name of names) {
|
|
30
|
-
const value = computed.getPropertyValue(name).trim();
|
|
31
|
-
if (value) to.style.setProperty(name, value);
|
|
32
|
-
else to.style.removeProperty(name);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
@@ -36,6 +36,11 @@ function createMockElement(tag = "div"): any {
|
|
|
36
36
|
clientHeight: 300,
|
|
37
37
|
// For <select>
|
|
38
38
|
options: [] as any[],
|
|
39
|
+
append(...nodes: (Node | string)[]) {
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
el.appendChild(typeof node === "string" ? { nodeType: 3, textContent: node } : node);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
39
44
|
appendChild(child: any) {
|
|
40
45
|
// Handle DocumentFragment: move its children into this element
|
|
41
46
|
if (child.tagName === "FRAGMENT") {
|
|
@@ -25,6 +25,11 @@ import { createMentionTokenElement } from "../utils/mention-token";
|
|
|
25
25
|
import { createIconButton } from "../utils/buttons";
|
|
26
26
|
import { renderLucideIcon } from "../utils/icons";
|
|
27
27
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT } from "../utils/content";
|
|
28
|
+
import {
|
|
29
|
+
applyUserCollapseState,
|
|
30
|
+
createUserCollapseRegion,
|
|
31
|
+
type UserMessageCollapseRender,
|
|
32
|
+
} from "./user-message-collapse";
|
|
28
33
|
import {
|
|
29
34
|
applyStreamBuffer,
|
|
30
35
|
createSkeletonPlaceholder,
|
|
@@ -998,6 +1003,11 @@ export type CreateStandardBubbleOptions = {
|
|
|
998
1003
|
* Full widget config (needed for loading indicator context)
|
|
999
1004
|
*/
|
|
1000
1005
|
widgetConfig?: import("../types").AgentWidgetConfig;
|
|
1006
|
+
/**
|
|
1007
|
+
* Long user message collapse (`features.userMessageCollapse`). When set on
|
|
1008
|
+
* a user bubble, its content is wrapped in a clip region with the toggle.
|
|
1009
|
+
*/
|
|
1010
|
+
userCollapse?: UserMessageCollapseRender;
|
|
1001
1011
|
};
|
|
1002
1012
|
|
|
1003
1013
|
/**
|
|
@@ -1238,6 +1248,12 @@ export const createStandardBubble = (
|
|
|
1238
1248
|
}
|
|
1239
1249
|
}
|
|
1240
1250
|
|
|
1251
|
+
// Everything a long user message can spill (attachments, chips, text) goes
|
|
1252
|
+
// in the collapse region; the timestamp and actions stay outside the clip.
|
|
1253
|
+
const userCollapse = message.role === "user" ? options?.userCollapse : undefined;
|
|
1254
|
+
const collapseRegion = userCollapse ? createUserCollapseRegion(message.id, userCollapse.maxHeight) : null;
|
|
1255
|
+
const contentParent = collapseRegion ?? bubble;
|
|
1256
|
+
|
|
1241
1257
|
if (imageParts.length > 0) {
|
|
1242
1258
|
const imagePreviews = createMessageImagePreviews(
|
|
1243
1259
|
imageParts,
|
|
@@ -1250,7 +1266,7 @@ export const createStandardBubble = (
|
|
|
1250
1266
|
);
|
|
1251
1267
|
|
|
1252
1268
|
if (imagePreviews) {
|
|
1253
|
-
|
|
1269
|
+
contentParent.appendChild(imagePreviews);
|
|
1254
1270
|
} else if (shouldHideTextUntilPreviewFails && textContentDiv) {
|
|
1255
1271
|
textContentDiv.style.display = "";
|
|
1256
1272
|
}
|
|
@@ -1260,7 +1276,7 @@ export const createStandardBubble = (
|
|
|
1260
1276
|
if (audioParts.length > 0) {
|
|
1261
1277
|
const audioPreviews = createMessageAudioPreviews(audioParts);
|
|
1262
1278
|
if (audioPreviews) {
|
|
1263
|
-
|
|
1279
|
+
contentParent.appendChild(audioPreviews);
|
|
1264
1280
|
}
|
|
1265
1281
|
}
|
|
1266
1282
|
|
|
@@ -1268,7 +1284,7 @@ export const createStandardBubble = (
|
|
|
1268
1284
|
if (videoParts.length > 0) {
|
|
1269
1285
|
const videoPreviews = createMessageVideoPreviews(videoParts);
|
|
1270
1286
|
if (videoPreviews) {
|
|
1271
|
-
|
|
1287
|
+
contentParent.appendChild(videoPreviews);
|
|
1272
1288
|
}
|
|
1273
1289
|
}
|
|
1274
1290
|
|
|
@@ -1276,7 +1292,7 @@ export const createStandardBubble = (
|
|
|
1276
1292
|
if (fileParts.length > 0) {
|
|
1277
1293
|
const filePreviews = createMessageFilePreviews(fileParts);
|
|
1278
1294
|
if (filePreviews) {
|
|
1279
|
-
|
|
1295
|
+
contentParent.appendChild(filePreviews);
|
|
1280
1296
|
}
|
|
1281
1297
|
}
|
|
1282
1298
|
|
|
@@ -1286,10 +1302,14 @@ export const createStandardBubble = (
|
|
|
1286
1302
|
? null
|
|
1287
1303
|
: createMessageMentionChips(message.contextMentions);
|
|
1288
1304
|
if (mentionChips) {
|
|
1289
|
-
|
|
1305
|
+
contentParent.appendChild(mentionChips);
|
|
1290
1306
|
}
|
|
1291
1307
|
|
|
1292
|
-
|
|
1308
|
+
contentParent.appendChild(contentDiv);
|
|
1309
|
+
if (collapseRegion && userCollapse) {
|
|
1310
|
+
bubble.appendChild(collapseRegion);
|
|
1311
|
+
applyUserCollapseState(bubble, collapseRegion, message.id, userCollapse);
|
|
1312
|
+
}
|
|
1293
1313
|
|
|
1294
1314
|
// Add timestamp below if configured
|
|
1295
1315
|
if (showTimestamp && timestampPosition === "below" && message.createdAt) {
|