@runtypelabs/persona 3.29.1 → 3.30.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.
Files changed (53) hide show
  1. package/README.md +55 -0
  2. package/dist/codegen.cjs +6 -6
  3. package/dist/codegen.js +4 -4
  4. package/dist/index.cjs +47 -47
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +112 -7
  7. package/dist/index.d.ts +112 -7
  8. package/dist/index.global.js +59 -59
  9. package/dist/index.global.js.map +1 -1
  10. package/dist/index.js +47 -47
  11. package/dist/index.js.map +1 -1
  12. package/dist/launcher.global.js +2 -2
  13. package/dist/launcher.global.js.map +1 -1
  14. package/dist/plugin-kit.cjs +1 -0
  15. package/dist/plugin-kit.d.cts +98 -0
  16. package/dist/plugin-kit.d.ts +98 -0
  17. package/dist/plugin-kit.js +1 -0
  18. package/dist/smart-dom-reader.d.cts +107 -4
  19. package/dist/smart-dom-reader.d.ts +107 -4
  20. package/dist/theme-editor.cjs +38 -38
  21. package/dist/theme-editor.d.cts +111 -6
  22. package/dist/theme-editor.d.ts +111 -6
  23. package/dist/theme-editor.js +38 -38
  24. package/dist/theme-reference.cjs +1 -1
  25. package/dist/theme-reference.d.cts +2 -0
  26. package/dist/theme-reference.d.ts +2 -0
  27. package/dist/theme-reference.js +1 -1
  28. package/package.json +8 -2
  29. package/src/components/approval-bubble.test.ts +268 -0
  30. package/src/components/approval-bubble.ts +170 -20
  31. package/src/components/launcher.ts +6 -3
  32. package/src/components/tool-bubble.test.ts +39 -0
  33. package/src/components/tool-bubble.ts +4 -0
  34. package/src/index-core.ts +1 -0
  35. package/src/plugin-kit.test.ts +230 -0
  36. package/src/plugin-kit.ts +294 -0
  37. package/src/plugins/types.ts +49 -2
  38. package/src/session.test.ts +99 -0
  39. package/src/session.ts +14 -3
  40. package/src/theme-editor/preview-utils.test.ts +10 -0
  41. package/src/theme-editor/preview-utils.ts +29 -1
  42. package/src/theme-editor/sections.test.ts +17 -0
  43. package/src/theme-editor/sections.ts +31 -0
  44. package/src/theme-reference.ts +1 -1
  45. package/src/types/theme.ts +2 -0
  46. package/src/types.ts +59 -2
  47. package/src/ui.approval-plugin.test.ts +204 -0
  48. package/src/ui.ts +149 -16
  49. package/src/utils/theme.test.ts +6 -2
  50. package/src/utils/theme.ts +0 -8
  51. package/src/utils/tokens.ts +6 -1
  52. package/src/webmcp-bridge.test.ts +66 -0
  53. package/src/webmcp-bridge.ts +49 -0
@@ -0,0 +1 @@
1
+ "use strict";var w=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var N=(e,t)=>{for(var o in t)w(e,o,{get:t[o],enumerable:!0})},I=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of H(t))!M.call(e,r)&&r!==o&&w(e,r,{get:()=>t[r],enumerable:!(n=C(t,r))||n.enumerable});return e};var O=e=>I(w({},"__esModule",{value:!0}),e);var B={};N(B,{createPopover:()=>$,getStyleRoot:()=>g,injectStyles:()=>z,isEditableEventTarget:()=>W});module.exports=O(B);function g(e){var o,n;let t=(o=e.getRootNode)==null?void 0:o.call(e);return t instanceof ShadowRoot||t instanceof Document?t:(n=e.ownerDocument)!=null?n:document}function v(e,t,o){var s;let n=e instanceof Document?e.head:e,r=t.replace(/["\\]/g,"\\$&");if(n.querySelector(`style[data-persona-plugin-style="${r}"]`))return;let c=(e instanceof Document?e:(s=e.ownerDocument)!=null?s:document).createElement("style");c.setAttribute("data-persona-plugin-style",t),c.textContent=o,n.appendChild(c)}function z(e,t,o){var l;if(e instanceof Document||e instanceof ShadowRoot){v(e,t,o);return}let n=e;if(n.isConnected){v(g(n),t,o);return}let r=(l=n.ownerDocument)!=null?l:document;v(r,t,o),queueMicrotask(()=>{let c=g(n);c!==r&&v(c,t,o)})}function A(e){var o,n;let t=(o=e.getRootNode)==null?void 0:o.call(e);return t instanceof ShadowRoot?t:((n=e.ownerDocument)!=null?n:document).body}function $(e){var y;let{anchor:t,content:o,placement:n="bottom-start",offset:r=6,matchAnchorWidth:l=!1,zIndex:c=2147483e3,onOpen:s,onDismiss:a}=e,S=(y=e.container)!=null?y:A(t),d=!1,m=null,h=()=>{if(!d)return;let i=t.getBoundingClientRect();o.style.position="fixed",l&&(o.style.minWidth=`${i.width}px`);let f=n==="top-start"||n==="top-end"?i.top-r-o.getBoundingClientRect().height:i.bottom+r,u=n==="bottom-end"||n==="top-end"?i.right-o.getBoundingClientRect().width:i.left;o.style.top=`${f}px`,o.style.left=`${u}px`},p=()=>{d&&(d=!1,m&&(m(),m=null),o.remove())},E=()=>{var x,b,P;if(d)return;d=!0,c!=null&&(o.style.zIndex=String(c)),S.appendChild(o),h();let i=(b=((x=t.ownerDocument)!=null?x:document).defaultView)!=null?b:window,f=(P=t.ownerDocument)!=null?P:document,u=()=>{if(!t.isConnected){p(),a==null||a("anchor-removed");return}h()},R=D=>{let L=typeof D.composedPath=="function"?D.composedPath():[];L.includes(o)||L.includes(t)||(p(),a==null||a("outside"))},T=i.setTimeout(()=>{f.addEventListener("pointerdown",R,!0)},0);i.addEventListener("scroll",u,!0),i.addEventListener("resize",u),m=()=>{i.clearTimeout(T),f.removeEventListener("pointerdown",R,!0),i.removeEventListener("scroll",u,!0),i.removeEventListener("resize",u)},s==null||s()};return{get isOpen(){return d},open:E,close:p,toggle:()=>d?p():E(),reposition:h,destroy:p}}function W(e){return(typeof e.composedPath=="function"?e.composedPath():[]).some(o=>o instanceof HTMLElement&&(o.tagName==="INPUT"||o.tagName==="TEXTAREA"||o.isContentEditable))}0&&(module.exports={createPopover,getStyleRoot,injectStyles,isEditableEventTarget});
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Resolve the root a node's styles should live in.
3
+ *
4
+ * Returns the node's `ShadowRoot` when it (or an ancestor) is shadowed, the
5
+ * owning `Document` otherwise. A `<style>` placed in this root reaches the node
6
+ * regardless of whether the widget runs in light or shadow DOM. For a detached
7
+ * node (one not yet mounted) this falls back to the owning document.
8
+ */
9
+ declare function getStyleRoot(node: Node): Document | ShadowRoot;
10
+ /**
11
+ * Inject a plugin's CSS once into the correct root — the widget's shadow root
12
+ * when shadowed, the document head otherwise. Idempotent: keyed by `id`, so it
13
+ * is safe to call on every render.
14
+ *
15
+ * Pass the element you're about to return from a render hook. While building, an
16
+ * element is detached and its eventual root is unknown, so this injects into the
17
+ * owning document immediately (covering the default light-DOM case with no
18
+ * flash) and then, on the next microtask — after the widget has mounted the
19
+ * element — re-resolves and also injects into its shadow root if it landed in
20
+ * one. You may also pass an explicit `Document` or `ShadowRoot`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * renderApproval: ({ message, approve, deny }) => {
25
+ * const card = buildCard(message.approval, approve, deny);
26
+ * injectStyles(card, "my-approval-plugin", CSS);
27
+ * return card;
28
+ * }
29
+ * ```
30
+ */
31
+ declare function injectStyles(target: Node | Document | ShadowRoot, id: string, css: string): void;
32
+ type PopoverPlacement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
33
+ interface PopoverOptions {
34
+ /** Element the popover is positioned against. */
35
+ anchor: HTMLElement;
36
+ /** The floating element (menu, tooltip, panel). Built detached; mounted on `open`. */
37
+ content: HTMLElement;
38
+ /** Where to place `content` relative to `anchor`. Default `"bottom-start"`. */
39
+ placement?: PopoverPlacement;
40
+ /** Gap in px between anchor and content. Default `6`. */
41
+ offset?: number;
42
+ /** Set `content`'s `min-width` to the anchor's width. Default `false`. */
43
+ matchAnchorWidth?: boolean;
44
+ /**
45
+ * Inline `z-index` for `content`. Default `2147483000` so it overlays the rest
46
+ * of the widget. Pass `null` to leave z-index to your own CSS.
47
+ */
48
+ zIndex?: number | null;
49
+ /**
50
+ * Where to mount `content`. Defaults to the anchor's shadow root (when
51
+ * shadowed) or `document.body` — keeping it inside the same style + stacking
52
+ * scope as the anchor while escaping the transcript's scroll clipping.
53
+ */
54
+ container?: HTMLElement | ShadowRoot;
55
+ onOpen?: () => void;
56
+ /** Fired when the popover closes by itself (outside click or the anchor leaving the DOM). */
57
+ onDismiss?: (reason: "outside" | "anchor-removed") => void;
58
+ }
59
+ interface PopoverHandle {
60
+ readonly isOpen: boolean;
61
+ open(): void;
62
+ close(): void;
63
+ toggle(): void;
64
+ /** Recompute position from the current anchor rect (called automatically on scroll/resize). */
65
+ reposition(): void;
66
+ /** Close and release all listeners; the handle is inert afterward. */
67
+ destroy(): void;
68
+ }
69
+ /**
70
+ * A floating popover anchored to an element: `fixed`-positioned (so it overlays
71
+ * the rest of the widget and isn't clipped by scroll containers), dismissed on
72
+ * outside pointerdown, repositioned on scroll/resize, and auto-closed if the
73
+ * anchor leaves the DOM. Mount your styles with {@link injectStyles}.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const popover = createPopover({
78
+ * anchor: splitButton,
79
+ * content: menu,
80
+ * placement: "bottom-start",
81
+ * matchAnchorWidth: true,
82
+ * });
83
+ * caret.addEventListener("click", () => popover.toggle());
84
+ * // on teardown: popover.destroy();
85
+ * ```
86
+ */
87
+ declare function createPopover(options: PopoverOptions): PopoverHandle;
88
+ /**
89
+ * Whether an event originated from an editable element (`<input>`, `<textarea>`,
90
+ * or `contenteditable`). Use it to avoid hijacking keys like Enter/Escape while
91
+ * the user is typing in the composer.
92
+ *
93
+ * Inspects the composed path, so it works for events that cross the widget's
94
+ * Shadow-DOM boundary (where `event.target` is retargeted to the host).
95
+ */
96
+ declare function isEditableEventTarget(event: Event): boolean;
97
+
98
+ export { type PopoverHandle, type PopoverOptions, type PopoverPlacement, createPopover, getStyleRoot, injectStyles, isEditableEventTarget };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Resolve the root a node's styles should live in.
3
+ *
4
+ * Returns the node's `ShadowRoot` when it (or an ancestor) is shadowed, the
5
+ * owning `Document` otherwise. A `<style>` placed in this root reaches the node
6
+ * regardless of whether the widget runs in light or shadow DOM. For a detached
7
+ * node (one not yet mounted) this falls back to the owning document.
8
+ */
9
+ declare function getStyleRoot(node: Node): Document | ShadowRoot;
10
+ /**
11
+ * Inject a plugin's CSS once into the correct root — the widget's shadow root
12
+ * when shadowed, the document head otherwise. Idempotent: keyed by `id`, so it
13
+ * is safe to call on every render.
14
+ *
15
+ * Pass the element you're about to return from a render hook. While building, an
16
+ * element is detached and its eventual root is unknown, so this injects into the
17
+ * owning document immediately (covering the default light-DOM case with no
18
+ * flash) and then, on the next microtask — after the widget has mounted the
19
+ * element — re-resolves and also injects into its shadow root if it landed in
20
+ * one. You may also pass an explicit `Document` or `ShadowRoot`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * renderApproval: ({ message, approve, deny }) => {
25
+ * const card = buildCard(message.approval, approve, deny);
26
+ * injectStyles(card, "my-approval-plugin", CSS);
27
+ * return card;
28
+ * }
29
+ * ```
30
+ */
31
+ declare function injectStyles(target: Node | Document | ShadowRoot, id: string, css: string): void;
32
+ type PopoverPlacement = "bottom-start" | "bottom-end" | "top-start" | "top-end";
33
+ interface PopoverOptions {
34
+ /** Element the popover is positioned against. */
35
+ anchor: HTMLElement;
36
+ /** The floating element (menu, tooltip, panel). Built detached; mounted on `open`. */
37
+ content: HTMLElement;
38
+ /** Where to place `content` relative to `anchor`. Default `"bottom-start"`. */
39
+ placement?: PopoverPlacement;
40
+ /** Gap in px between anchor and content. Default `6`. */
41
+ offset?: number;
42
+ /** Set `content`'s `min-width` to the anchor's width. Default `false`. */
43
+ matchAnchorWidth?: boolean;
44
+ /**
45
+ * Inline `z-index` for `content`. Default `2147483000` so it overlays the rest
46
+ * of the widget. Pass `null` to leave z-index to your own CSS.
47
+ */
48
+ zIndex?: number | null;
49
+ /**
50
+ * Where to mount `content`. Defaults to the anchor's shadow root (when
51
+ * shadowed) or `document.body` — keeping it inside the same style + stacking
52
+ * scope as the anchor while escaping the transcript's scroll clipping.
53
+ */
54
+ container?: HTMLElement | ShadowRoot;
55
+ onOpen?: () => void;
56
+ /** Fired when the popover closes by itself (outside click or the anchor leaving the DOM). */
57
+ onDismiss?: (reason: "outside" | "anchor-removed") => void;
58
+ }
59
+ interface PopoverHandle {
60
+ readonly isOpen: boolean;
61
+ open(): void;
62
+ close(): void;
63
+ toggle(): void;
64
+ /** Recompute position from the current anchor rect (called automatically on scroll/resize). */
65
+ reposition(): void;
66
+ /** Close and release all listeners; the handle is inert afterward. */
67
+ destroy(): void;
68
+ }
69
+ /**
70
+ * A floating popover anchored to an element: `fixed`-positioned (so it overlays
71
+ * the rest of the widget and isn't clipped by scroll containers), dismissed on
72
+ * outside pointerdown, repositioned on scroll/resize, and auto-closed if the
73
+ * anchor leaves the DOM. Mount your styles with {@link injectStyles}.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const popover = createPopover({
78
+ * anchor: splitButton,
79
+ * content: menu,
80
+ * placement: "bottom-start",
81
+ * matchAnchorWidth: true,
82
+ * });
83
+ * caret.addEventListener("click", () => popover.toggle());
84
+ * // on teardown: popover.destroy();
85
+ * ```
86
+ */
87
+ declare function createPopover(options: PopoverOptions): PopoverHandle;
88
+ /**
89
+ * Whether an event originated from an editable element (`<input>`, `<textarea>`,
90
+ * or `contenteditable`). Use it to avoid hijacking keys like Enter/Escape while
91
+ * the user is typing in the composer.
92
+ *
93
+ * Inspects the composed path, so it works for events that cross the widget's
94
+ * Shadow-DOM boundary (where `event.target` is retargeted to the host).
95
+ */
96
+ declare function isEditableEventTarget(event: Event): boolean;
97
+
98
+ export { type PopoverHandle, type PopoverOptions, type PopoverPlacement, createPopover, getStyleRoot, injectStyles, isEditableEventTarget };
@@ -0,0 +1 @@
1
+ function D(t){var e,n;let o=(e=t.getRootNode)==null?void 0:e.call(t);return o instanceof ShadowRoot||o instanceof Document?o:(n=t.ownerDocument)!=null?n:document}function v(t,o,e){var s;let n=t instanceof Document?t.head:t,c=o.replace(/["\\]/g,"\\$&");if(n.querySelector(`style[data-persona-plugin-style="${c}"]`))return;let i=(t instanceof Document?t:(s=t.ownerDocument)!=null?s:document).createElement("style");i.setAttribute("data-persona-plugin-style",o),i.textContent=e,n.appendChild(i)}function C(t,o,e){var l;if(t instanceof Document||t instanceof ShadowRoot){v(t,o,e);return}let n=t;if(n.isConnected){v(D(n),o,e);return}let c=(l=n.ownerDocument)!=null?l:document;v(c,o,e),queueMicrotask(()=>{let i=D(n);i!==c&&v(i,o,e)})}function T(t){var e,n;let o=(e=t.getRootNode)==null?void 0:e.call(t);return o instanceof ShadowRoot?o:((n=t.ownerDocument)!=null?n:document).body}function H(t){var g;let{anchor:o,content:e,placement:n="bottom-start",offset:c=6,matchAnchorWidth:l=!1,zIndex:i=2147483e3,onOpen:s,onDismiss:a}=t,L=(g=t.container)!=null?g:T(o),d=!1,m=null,h=()=>{if(!d)return;let r=o.getBoundingClientRect();e.style.position="fixed",l&&(e.style.minWidth=`${r.width}px`);let f=n==="top-start"||n==="top-end"?r.top-c-e.getBoundingClientRect().height:r.bottom+c,u=n==="bottom-end"||n==="top-end"?r.right-e.getBoundingClientRect().width:r.left;e.style.top=`${f}px`,e.style.left=`${u}px`},p=()=>{d&&(d=!1,m&&(m(),m=null),e.remove())},w=()=>{var y,R,x;if(d)return;d=!0,i!=null&&(e.style.zIndex=String(i)),L.appendChild(e),h();let r=(R=((y=o.ownerDocument)!=null?y:document).defaultView)!=null?R:window,f=(x=o.ownerDocument)!=null?x:document,u=()=>{if(!o.isConnected){p(),a==null||a("anchor-removed");return}h()},E=b=>{let P=typeof b.composedPath=="function"?b.composedPath():[];P.includes(e)||P.includes(o)||(p(),a==null||a("outside"))},S=r.setTimeout(()=>{f.addEventListener("pointerdown",E,!0)},0);r.addEventListener("scroll",u,!0),r.addEventListener("resize",u),m=()=>{r.clearTimeout(S),f.removeEventListener("pointerdown",E,!0),r.removeEventListener("scroll",u,!0),r.removeEventListener("resize",u)},s==null||s()};return{get isOpen(){return d},open:w,close:p,toggle:()=>d?p():w(),reposition:h,destroy:p}}function M(t){return(typeof t.composedPath=="function"?t.composedPath():[]).some(e=>e instanceof HTMLElement&&(e.tagName==="INPUT"||e.tagName==="TEXTAREA"||e.isContentEditable))}export{H as createPopover,D as getStyleRoot,C as injectStyles,M as isEditableEventTarget};
@@ -416,13 +416,59 @@ interface AgentWidgetPlugin {
416
416
  config: AgentWidgetConfig;
417
417
  }) => HTMLElement | null;
418
418
  /**
419
- * Custom renderer for approval bubbles
420
- * Return null to use default renderer
419
+ * Custom renderer for approval bubbles.
420
+ *
421
+ * Return an `HTMLElement` to fully own the approval UI, `defaultRenderer()`
422
+ * to render (or wrap) the built-in bubble, or `null` to fall through to the
423
+ * default. Unlike the built-in bubble — whose Approve/Deny buttons are wired
424
+ * via delegation — a fully custom element resolves the approval by calling
425
+ * the `approve`/`deny` callbacks. Both route through the same path the
426
+ * built-in buttons use (optimistic update, `onDecision`, in-place anchoring).
427
+ *
428
+ * An approval is a single binary gate, so there are exactly two outcomes.
429
+ * Pass `{ remember: true }` to flag a "remember this" affordance (e.g. an
430
+ * "Always allow" button); the current approval resolves identically, but the
431
+ * flag is forwarded to `config.approval.onDecision` so you can persist a
432
+ * don't-ask-again policy for future approvals.
433
+ *
434
+ * `renderApproval` is called again whenever the approval's status changes, so
435
+ * branch on `message.approval?.status` to render the resolved state (and tear
436
+ * down any global listeners you added while pending).
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * // An alternative prompt: "Always allow" / "Allow once" / "Deny".
441
+ * renderApproval: ({ message, approve, deny }) => {
442
+ * const approval = message.approval;
443
+ * if (!approval || approval.status !== "pending") return null; // default renders resolved state
444
+ * const root = document.createElement("div");
445
+ * root.textContent = `${approval.toolName} requires approval`;
446
+ *
447
+ * const always = document.createElement("button");
448
+ * always.textContent = "Always allow";
449
+ * always.addEventListener("click", () => approve({ remember: true }));
450
+ *
451
+ * const once = document.createElement("button");
452
+ * once.textContent = "Allow once";
453
+ * once.addEventListener("click", () => approve());
454
+ *
455
+ * const no = document.createElement("button");
456
+ * no.textContent = "Deny";
457
+ * no.addEventListener("click", () => deny());
458
+ *
459
+ * root.append(always, once, no);
460
+ * return root;
461
+ * }
462
+ * ```
421
463
  */
422
464
  renderApproval?: (context: {
423
465
  message: AgentWidgetMessage;
424
466
  defaultRenderer: () => HTMLElement;
425
467
  config: AgentWidgetConfig;
468
+ /** Resolve this approval as approved. Pass `{ remember: true }` for an "Always allow" affordance. */
469
+ approve: (options?: AgentWidgetApprovalDecisionOptions) => void;
470
+ /** Resolve this approval as denied. Pass `{ remember: true }` for an "Always deny" affordance. */
471
+ deny: (options?: AgentWidgetApprovalDecisionOptions) => void;
426
472
  }) => HTMLElement | null;
427
473
  /**
428
474
  * Custom renderer for loading indicator
@@ -797,6 +843,8 @@ interface ApprovalTokens {
797
843
  background: TokenReference<'color'>;
798
844
  border: TokenReference<'color'>;
799
845
  text: TokenReference<'color'>;
846
+ /** Box-shadow for the approval bubble (token ref or raw CSS, e.g. `none`). */
847
+ shadow: string;
800
848
  };
801
849
  approve: ComponentTokenSet;
802
850
  deny: ComponentTokenSet;
@@ -1715,6 +1763,12 @@ type WebMcpConfirmInfo = {
1715
1763
  toolName: string;
1716
1764
  args: unknown;
1717
1765
  description?: string;
1766
+ /**
1767
+ * Display title the tool declared via the WebMCP spec's
1768
+ * `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool
1769
+ * didn't declare one.
1770
+ */
1771
+ title?: string;
1718
1772
  annotations?: {
1719
1773
  readOnlyHint?: boolean;
1720
1774
  untrustedContentHint?: boolean;
@@ -3029,6 +3083,22 @@ type TextToSpeechConfig = {
3029
3083
  /** Speech pitch (0 - 2). Default: 1 */
3030
3084
  pitch?: number;
3031
3085
  };
3086
+ /**
3087
+ * Extra context for an approval decision, surfaced to `onDecision` and to the
3088
+ * `approve`/`deny` callbacks passed to the `renderApproval` plugin hook.
3089
+ */
3090
+ type AgentWidgetApprovalDecisionOptions = {
3091
+ /**
3092
+ * The user chose a "remember this" affordance (e.g. an "Always allow"
3093
+ * button) rather than a one-time decision. The widget resolves the *current*
3094
+ * approval identically whether or not this is set — an approval bubble is a
3095
+ * single binary gate (`approved`/`denied`). Persisting a don't-ask-again
3096
+ * policy for *future* approvals (auto-resolving them, or not surfacing them)
3097
+ * is the integrator's responsibility, typically inside `onDecision`.
3098
+ * Defaults to absent/`false`.
3099
+ */
3100
+ remember?: boolean;
3101
+ };
3032
3102
  /**
3033
3103
  * Configuration for tool approval bubbles.
3034
3104
  * Controls styling, labels, and behavior of the approval UI.
@@ -3038,6 +3108,8 @@ type AgentWidgetApprovalConfig = {
3038
3108
  backgroundColor?: string;
3039
3109
  /** Border color of the approval bubble */
3040
3110
  borderColor?: string;
3111
+ /** Box-shadow for the approval bubble; pass `"none"` to remove it. Overrides the default `persona-shadow-sm`. */
3112
+ shadow?: string;
3041
3113
  /** Color for the title text */
3042
3114
  titleColor?: string;
3043
3115
  /** Color for the description text */
@@ -3060,20 +3132,51 @@ type AgentWidgetApprovalConfig = {
3060
3132
  approveLabel?: string;
3061
3133
  /** Label for the deny button */
3062
3134
  denyLabel?: string;
3135
+ /**
3136
+ * How the technical details (the tool's agent-facing description and the
3137
+ * raw parameters JSON) are presented:
3138
+ * - `"collapsed"` (default) — hidden behind a "Show details" toggle
3139
+ * - `"expanded"` — visible, with the toggle available to hide them
3140
+ * - `"hidden"` — never rendered
3141
+ */
3142
+ detailsDisplay?: "collapsed" | "expanded" | "hidden";
3143
+ /** Label for the toggle that reveals the technical details */
3144
+ showDetailsLabel?: string;
3145
+ /** Label for the toggle that hides the technical details */
3146
+ hideDetailsLabel?: string;
3147
+ /**
3148
+ * Build the user-facing summary line for an approval request. Overrides the
3149
+ * default "The assistant wants to use “Tool name”." copy. Return a falsy
3150
+ * value to fall back to the default for that approval. `displayTitle` is
3151
+ * the display name the tool declared via the WebMCP spec's
3152
+ * `ToolDescriptor.title`, when one exists.
3153
+ */
3154
+ formatDescription?: (approval: {
3155
+ toolName: string;
3156
+ toolType?: string;
3157
+ description: string;
3158
+ parameters?: unknown;
3159
+ displayTitle?: string;
3160
+ }) => string | undefined;
3063
3161
  /**
3064
3162
  * Custom handler for approval decisions.
3065
3163
  * Return void to let the SDK auto-resolve via the API,
3066
3164
  * or return a Response/ReadableStream for custom handling.
3165
+ *
3166
+ * `options.remember` is `true` when the decision came from a "remember this"
3167
+ * affordance (e.g. an "Always allow" button in a custom `renderApproval`
3168
+ * plugin). Use it to persist a don't-ask-again policy for future approvals;
3169
+ * the current approval resolves the same way regardless.
3067
3170
  */
3068
3171
  onDecision?: (data: {
3069
3172
  approvalId: string;
3070
3173
  executionId: string;
3071
3174
  agentId: string;
3072
3175
  toolName: string;
3073
- }, decision: 'approved' | 'denied') => Promise<Response | ReadableStream<Uint8Array> | void>;
3176
+ }, decision: 'approved' | 'denied', options?: AgentWidgetApprovalDecisionOptions) => Promise<Response | ReadableStream<Uint8Array> | void>;
3074
3177
  };
3075
3178
  type AgentWidgetToolCallConfig = {
3076
- /** Box-shadow for tool-call bubbles; overrides `theme.toolBubbleShadow` when set. */
3179
+ /** Box-shadow for tool-call bubbles; pass `"none"` to remove it. Overrides the `components.toolBubble.shadow` token / `--persona-tool-bubble-shadow`. */
3077
3180
  shadow?: string;
3078
3181
  /** Background color of the tool call bubble container. */
3079
3182
  backgroundColor?: string;
@@ -416,13 +416,59 @@ interface AgentWidgetPlugin {
416
416
  config: AgentWidgetConfig;
417
417
  }) => HTMLElement | null;
418
418
  /**
419
- * Custom renderer for approval bubbles
420
- * Return null to use default renderer
419
+ * Custom renderer for approval bubbles.
420
+ *
421
+ * Return an `HTMLElement` to fully own the approval UI, `defaultRenderer()`
422
+ * to render (or wrap) the built-in bubble, or `null` to fall through to the
423
+ * default. Unlike the built-in bubble — whose Approve/Deny buttons are wired
424
+ * via delegation — a fully custom element resolves the approval by calling
425
+ * the `approve`/`deny` callbacks. Both route through the same path the
426
+ * built-in buttons use (optimistic update, `onDecision`, in-place anchoring).
427
+ *
428
+ * An approval is a single binary gate, so there are exactly two outcomes.
429
+ * Pass `{ remember: true }` to flag a "remember this" affordance (e.g. an
430
+ * "Always allow" button); the current approval resolves identically, but the
431
+ * flag is forwarded to `config.approval.onDecision` so you can persist a
432
+ * don't-ask-again policy for future approvals.
433
+ *
434
+ * `renderApproval` is called again whenever the approval's status changes, so
435
+ * branch on `message.approval?.status` to render the resolved state (and tear
436
+ * down any global listeners you added while pending).
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * // An alternative prompt: "Always allow" / "Allow once" / "Deny".
441
+ * renderApproval: ({ message, approve, deny }) => {
442
+ * const approval = message.approval;
443
+ * if (!approval || approval.status !== "pending") return null; // default renders resolved state
444
+ * const root = document.createElement("div");
445
+ * root.textContent = `${approval.toolName} requires approval`;
446
+ *
447
+ * const always = document.createElement("button");
448
+ * always.textContent = "Always allow";
449
+ * always.addEventListener("click", () => approve({ remember: true }));
450
+ *
451
+ * const once = document.createElement("button");
452
+ * once.textContent = "Allow once";
453
+ * once.addEventListener("click", () => approve());
454
+ *
455
+ * const no = document.createElement("button");
456
+ * no.textContent = "Deny";
457
+ * no.addEventListener("click", () => deny());
458
+ *
459
+ * root.append(always, once, no);
460
+ * return root;
461
+ * }
462
+ * ```
421
463
  */
422
464
  renderApproval?: (context: {
423
465
  message: AgentWidgetMessage;
424
466
  defaultRenderer: () => HTMLElement;
425
467
  config: AgentWidgetConfig;
468
+ /** Resolve this approval as approved. Pass `{ remember: true }` for an "Always allow" affordance. */
469
+ approve: (options?: AgentWidgetApprovalDecisionOptions) => void;
470
+ /** Resolve this approval as denied. Pass `{ remember: true }` for an "Always deny" affordance. */
471
+ deny: (options?: AgentWidgetApprovalDecisionOptions) => void;
426
472
  }) => HTMLElement | null;
427
473
  /**
428
474
  * Custom renderer for loading indicator
@@ -797,6 +843,8 @@ interface ApprovalTokens {
797
843
  background: TokenReference<'color'>;
798
844
  border: TokenReference<'color'>;
799
845
  text: TokenReference<'color'>;
846
+ /** Box-shadow for the approval bubble (token ref or raw CSS, e.g. `none`). */
847
+ shadow: string;
800
848
  };
801
849
  approve: ComponentTokenSet;
802
850
  deny: ComponentTokenSet;
@@ -1715,6 +1763,12 @@ type WebMcpConfirmInfo = {
1715
1763
  toolName: string;
1716
1764
  args: unknown;
1717
1765
  description?: string;
1766
+ /**
1767
+ * Display title the tool declared via the WebMCP spec's
1768
+ * `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool
1769
+ * didn't declare one.
1770
+ */
1771
+ title?: string;
1718
1772
  annotations?: {
1719
1773
  readOnlyHint?: boolean;
1720
1774
  untrustedContentHint?: boolean;
@@ -3029,6 +3083,22 @@ type TextToSpeechConfig = {
3029
3083
  /** Speech pitch (0 - 2). Default: 1 */
3030
3084
  pitch?: number;
3031
3085
  };
3086
+ /**
3087
+ * Extra context for an approval decision, surfaced to `onDecision` and to the
3088
+ * `approve`/`deny` callbacks passed to the `renderApproval` plugin hook.
3089
+ */
3090
+ type AgentWidgetApprovalDecisionOptions = {
3091
+ /**
3092
+ * The user chose a "remember this" affordance (e.g. an "Always allow"
3093
+ * button) rather than a one-time decision. The widget resolves the *current*
3094
+ * approval identically whether or not this is set — an approval bubble is a
3095
+ * single binary gate (`approved`/`denied`). Persisting a don't-ask-again
3096
+ * policy for *future* approvals (auto-resolving them, or not surfacing them)
3097
+ * is the integrator's responsibility, typically inside `onDecision`.
3098
+ * Defaults to absent/`false`.
3099
+ */
3100
+ remember?: boolean;
3101
+ };
3032
3102
  /**
3033
3103
  * Configuration for tool approval bubbles.
3034
3104
  * Controls styling, labels, and behavior of the approval UI.
@@ -3038,6 +3108,8 @@ type AgentWidgetApprovalConfig = {
3038
3108
  backgroundColor?: string;
3039
3109
  /** Border color of the approval bubble */
3040
3110
  borderColor?: string;
3111
+ /** Box-shadow for the approval bubble; pass `"none"` to remove it. Overrides the default `persona-shadow-sm`. */
3112
+ shadow?: string;
3041
3113
  /** Color for the title text */
3042
3114
  titleColor?: string;
3043
3115
  /** Color for the description text */
@@ -3060,20 +3132,51 @@ type AgentWidgetApprovalConfig = {
3060
3132
  approveLabel?: string;
3061
3133
  /** Label for the deny button */
3062
3134
  denyLabel?: string;
3135
+ /**
3136
+ * How the technical details (the tool's agent-facing description and the
3137
+ * raw parameters JSON) are presented:
3138
+ * - `"collapsed"` (default) — hidden behind a "Show details" toggle
3139
+ * - `"expanded"` — visible, with the toggle available to hide them
3140
+ * - `"hidden"` — never rendered
3141
+ */
3142
+ detailsDisplay?: "collapsed" | "expanded" | "hidden";
3143
+ /** Label for the toggle that reveals the technical details */
3144
+ showDetailsLabel?: string;
3145
+ /** Label for the toggle that hides the technical details */
3146
+ hideDetailsLabel?: string;
3147
+ /**
3148
+ * Build the user-facing summary line for an approval request. Overrides the
3149
+ * default "The assistant wants to use “Tool name”." copy. Return a falsy
3150
+ * value to fall back to the default for that approval. `displayTitle` is
3151
+ * the display name the tool declared via the WebMCP spec's
3152
+ * `ToolDescriptor.title`, when one exists.
3153
+ */
3154
+ formatDescription?: (approval: {
3155
+ toolName: string;
3156
+ toolType?: string;
3157
+ description: string;
3158
+ parameters?: unknown;
3159
+ displayTitle?: string;
3160
+ }) => string | undefined;
3063
3161
  /**
3064
3162
  * Custom handler for approval decisions.
3065
3163
  * Return void to let the SDK auto-resolve via the API,
3066
3164
  * or return a Response/ReadableStream for custom handling.
3165
+ *
3166
+ * `options.remember` is `true` when the decision came from a "remember this"
3167
+ * affordance (e.g. an "Always allow" button in a custom `renderApproval`
3168
+ * plugin). Use it to persist a don't-ask-again policy for future approvals;
3169
+ * the current approval resolves the same way regardless.
3067
3170
  */
3068
3171
  onDecision?: (data: {
3069
3172
  approvalId: string;
3070
3173
  executionId: string;
3071
3174
  agentId: string;
3072
3175
  toolName: string;
3073
- }, decision: 'approved' | 'denied') => Promise<Response | ReadableStream<Uint8Array> | void>;
3176
+ }, decision: 'approved' | 'denied', options?: AgentWidgetApprovalDecisionOptions) => Promise<Response | ReadableStream<Uint8Array> | void>;
3074
3177
  };
3075
3178
  type AgentWidgetToolCallConfig = {
3076
- /** Box-shadow for tool-call bubbles; overrides `theme.toolBubbleShadow` when set. */
3179
+ /** Box-shadow for tool-call bubbles; pass `"none"` to remove it. Overrides the `components.toolBubble.shadow` token / `--persona-tool-bubble-shadow`. */
3077
3180
  shadow?: string;
3078
3181
  /** Background color of the tool call bubble container. */
3079
3182
  backgroundColor?: string;