@runtypelabs/persona 3.29.1 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2547
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_Qazlm4.d.cts → types-quh7NmYD.d.cts} +9 -0
- package/dist/animations/{types-B_Qazlm4.d.ts → types-quh7NmYD.d.ts} +9 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +4 -4
- package/dist/index.cjs +50 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +164 -7
- package/dist/index.d.ts +164 -7
- package/dist/index.global.js +60 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +49 -49
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -0
- package/dist/plugin-kit.d.cts +98 -0
- package/dist/plugin-kit.d.ts +98 -0
- package/dist/plugin-kit.js +1 -0
- package/dist/smart-dom-reader.d.cts +157 -4
- package/dist/smart-dom-reader.d.ts +157 -4
- package/dist/theme-editor.cjs +40 -40
- package/dist/theme-editor.d.cts +161 -6
- package/dist/theme-editor.d.ts +161 -6
- package/dist/theme-editor.js +40 -40
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +19 -0
- package/package.json +8 -2
- package/src/client.ts +6 -0
- package/src/components/approval-bubble.test.ts +320 -0
- package/src/components/approval-bubble.ts +190 -20
- package/src/components/launcher.ts +6 -3
- package/src/components/panel.ts +7 -1
- package/src/components/tool-bubble.test.ts +39 -0
- package/src/components/tool-bubble.ts +4 -0
- package/src/defaults.ts +14 -0
- package/src/generated/runtype-openapi-contract.ts +4 -0
- package/src/index-core.ts +1 -0
- package/src/plugin-kit.test.ts +230 -0
- package/src/plugin-kit.ts +294 -0
- package/src/plugins/types.ts +49 -2
- package/src/session.test.ts +161 -0
- package/src/session.ts +41 -3
- package/src/styles/widget.css +19 -0
- package/src/theme-editor/preview-utils.test.ts +10 -0
- package/src/theme-editor/preview-utils.ts +29 -1
- package/src/theme-editor/sections.test.ts +17 -0
- package/src/theme-editor/sections.ts +31 -0
- package/src/theme-reference.ts +2 -2
- package/src/types/theme.ts +2 -0
- package/src/types.ts +109 -2
- package/src/ui.approval-plugin.test.ts +204 -0
- package/src/ui.scroll.test.ts +383 -0
- package/src/ui.ts +539 -56
- package/src/utils/auto-follow.test.ts +91 -0
- package/src/utils/auto-follow.ts +68 -0
- package/src/utils/theme.test.ts +6 -2
- package/src/utils/theme.ts +0 -8
- package/src/utils/tokens.ts +6 -1
- package/src/webmcp-bridge.test.ts +66 -0
- package/src/webmcp-bridge.ts +49 -0
|
@@ -206,7 +206,8 @@ export type PreviewTranscriptEntryPreset =
|
|
|
206
206
|
| 'reasoning-streaming'
|
|
207
207
|
| 'reasoning-complete'
|
|
208
208
|
| 'tool-running'
|
|
209
|
-
| 'tool-complete'
|
|
209
|
+
| 'tool-complete'
|
|
210
|
+
| 'approval-request';
|
|
210
211
|
|
|
211
212
|
const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, string> = {
|
|
212
213
|
'user-message': 'User message',
|
|
@@ -218,6 +219,7 @@ const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, str
|
|
|
218
219
|
'reasoning-complete': 'Reasoning (complete)',
|
|
219
220
|
'tool-running': 'Tool call (running)',
|
|
220
221
|
'tool-complete': 'Tool call (complete)',
|
|
222
|
+
'approval-request': 'Approval request',
|
|
221
223
|
};
|
|
222
224
|
|
|
223
225
|
export function getPreviewTranscriptPresetLabel(preset: PreviewTranscriptEntryPreset): string {
|
|
@@ -325,6 +327,32 @@ export function createPreviewTranscriptEntry(
|
|
|
325
327
|
durationMs: 1200,
|
|
326
328
|
},
|
|
327
329
|
};
|
|
330
|
+
case 'approval-request': {
|
|
331
|
+
// Use the `approval-<approvalId>` id convention so the optimistic status
|
|
332
|
+
// flip in `session.resolveApproval` updates this bubble in place when the
|
|
333
|
+
// Approve/Deny buttons are clicked.
|
|
334
|
+
const approvalId = `preview-approval-${suffix}`;
|
|
335
|
+
return {
|
|
336
|
+
id: `approval-${approvalId}`,
|
|
337
|
+
role: 'assistant',
|
|
338
|
+
content: '',
|
|
339
|
+
createdAt,
|
|
340
|
+
streaming: false,
|
|
341
|
+
variant: 'approval',
|
|
342
|
+
approval: {
|
|
343
|
+
id: approvalId,
|
|
344
|
+
status: 'pending',
|
|
345
|
+
agentId: 'preview-agent',
|
|
346
|
+
executionId: `preview-exec-${suffix}`,
|
|
347
|
+
toolName: 'add_to_cart',
|
|
348
|
+
description:
|
|
349
|
+
'Add the selected item to the shopping cart. Approve to let the assistant continue.',
|
|
350
|
+
parameters: {
|
|
351
|
+
items: [{ productEntityId: 129, quantity: 1 }],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
328
356
|
case 'tool-complete':
|
|
329
357
|
return {
|
|
330
358
|
id: `preview-seq-tool-complete-${suffix}`,
|
|
@@ -27,6 +27,23 @@ describe("theme editor scroll-to-bottom controls", () => {
|
|
|
27
27
|
expect(fieldPaths).toContain("theme.components.scrollToBottom.iconSize");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("exposes a shadow control for every themeable component", () => {
|
|
31
|
+
const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
|
|
32
|
+
|
|
33
|
+
// Pre-existing shadow controls.
|
|
34
|
+
expect(fieldPaths).toContain("theme.components.launcher.shadow");
|
|
35
|
+
expect(fieldPaths).toContain("theme.components.panel.shadow");
|
|
36
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.shadow");
|
|
37
|
+
// Newly added component shadow controls.
|
|
38
|
+
expect(fieldPaths).toContain("theme.components.message.user.shadow");
|
|
39
|
+
expect(fieldPaths).toContain("theme.components.message.assistant.shadow");
|
|
40
|
+
expect(fieldPaths).toContain("theme.components.toolBubble.shadow");
|
|
41
|
+
expect(fieldPaths).toContain("theme.components.reasoningBubble.shadow");
|
|
42
|
+
expect(fieldPaths).toContain("theme.components.approval.requested.shadow");
|
|
43
|
+
expect(fieldPaths).toContain("theme.components.introCard.shadow");
|
|
44
|
+
expect(fieldPaths).toContain("theme.components.composer.shadow");
|
|
45
|
+
});
|
|
46
|
+
|
|
30
47
|
it("adds a scroll-to-bottom interface role mapping", () => {
|
|
31
48
|
const role = ALL_ROLES.find((entry) => entry.roleId === "role-scroll-to-bottom");
|
|
32
49
|
|
|
@@ -455,6 +455,36 @@ const scrollToBottomSectionDef: SectionDef = {
|
|
|
455
455
|
],
|
|
456
456
|
};
|
|
457
457
|
|
|
458
|
+
// Reusable shadow-scale options matching the launcher / panel / scroll-to-bottom selects.
|
|
459
|
+
const SHADOW_SCALE_OPTIONS: { value: string; label: string }[] = [
|
|
460
|
+
{ value: 'palette.shadows.none', label: 'None' },
|
|
461
|
+
{ value: 'palette.shadows.sm', label: 'Small' },
|
|
462
|
+
{ value: 'palette.shadows.md', label: 'Medium' },
|
|
463
|
+
{ value: 'palette.shadows.lg', label: 'Large' },
|
|
464
|
+
{ value: 'palette.shadows.xl', label: 'Extra Large' },
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
// The approval and intro-card defaults are a bespoke "card" shadow rather than a
|
|
468
|
+
// palette step, so expose it as a leading "Default" option users can keep.
|
|
469
|
+
const CARD_SHADOW = '0 5px 15px rgba(15, 23, 42, 0.08)';
|
|
470
|
+
const CARD_SHADOW_OPTIONS = [{ value: CARD_SHADOW, label: 'Default' }, ...SHADOW_SCALE_OPTIONS];
|
|
471
|
+
|
|
472
|
+
const componentShadowsSectionDef: SectionDef = {
|
|
473
|
+
id: 'comp-shadows',
|
|
474
|
+
title: 'Component Shadows',
|
|
475
|
+
description: 'Box-shadow for chat bubbles and surfaces.',
|
|
476
|
+
collapsed: true,
|
|
477
|
+
fields: [
|
|
478
|
+
{ id: 'shadow-msg-user', label: 'User Message', type: 'select', path: 'theme.components.message.user.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
479
|
+
{ id: 'shadow-msg-assistant', label: 'Assistant Message', type: 'select', path: 'theme.components.message.assistant.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
480
|
+
{ id: 'shadow-tool-bubble', label: 'Tool Call Bubble', type: 'select', path: 'theme.components.toolBubble.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
481
|
+
{ id: 'shadow-reasoning-bubble', label: 'Reasoning Bubble', type: 'select', path: 'theme.components.reasoningBubble.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
482
|
+
{ id: 'shadow-approval', label: 'Approval Bubble', type: 'select', path: 'theme.components.approval.requested.shadow', defaultValue: CARD_SHADOW, options: CARD_SHADOW_OPTIONS },
|
|
483
|
+
{ id: 'shadow-intro-card', label: 'Intro Card', type: 'select', path: 'theme.components.introCard.shadow', defaultValue: CARD_SHADOW, options: CARD_SHADOW_OPTIONS },
|
|
484
|
+
{ id: 'shadow-composer', label: 'Composer', type: 'select', path: 'theme.components.composer.shadow', defaultValue: 'palette.shadows.none', options: SHADOW_SCALE_OPTIONS },
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
|
|
458
488
|
/** Shared shape sections (not scoped to light/dark) */
|
|
459
489
|
export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
|
|
460
490
|
panelLayoutSectionDef,
|
|
@@ -462,6 +492,7 @@ export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
|
|
|
462
492
|
messageShapeSectionDef,
|
|
463
493
|
inputShapeSectionDef,
|
|
464
494
|
buttonShapeSectionDef,
|
|
495
|
+
componentShadowsSectionDef,
|
|
465
496
|
];
|
|
466
497
|
|
|
467
498
|
/** Component color sections (can be scoped for light/dark) */
|
package/src/theme-reference.ts
CHANGED
|
@@ -136,7 +136,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
136
136
|
voice:
|
|
137
137
|
'recording (indicator, background, border), processing (icon, background), speaking (icon).',
|
|
138
138
|
approval:
|
|
139
|
-
'requested (background, border, text), approve (background, foreground), deny (background, foreground).',
|
|
139
|
+
'requested (background, border, text, shadow), approve (background, foreground), deny (background, foreground).',
|
|
140
140
|
attachment: 'image (background, border).',
|
|
141
141
|
scrollToBottom:
|
|
142
142
|
'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
|
|
@@ -229,7 +229,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
229
229
|
description:
|
|
230
230
|
'Tool approval bubble styling and behavior. Set to false to disable.',
|
|
231
231
|
properties:
|
|
232
|
-
'backgroundColor, borderColor, titleColor, descriptionColor, approveButtonColor, approveButtonTextColor, denyButtonColor, denyButtonTextColor, parameterBackgroundColor, parameterTextColor, title, approveLabel, denyLabel.',
|
|
232
|
+
'backgroundColor, borderColor, titleColor, descriptionColor, reasonColor, reasonLabel, approveButtonColor, approveButtonTextColor, denyButtonColor, denyButtonTextColor, parameterBackgroundColor, parameterTextColor, title, approveLabel, denyLabel.',
|
|
233
233
|
},
|
|
234
234
|
copy: {
|
|
235
235
|
description: 'Widget text content.',
|
package/src/types/theme.ts
CHANGED
|
@@ -330,6 +330,8 @@ export interface ApprovalTokens {
|
|
|
330
330
|
background: TokenReference<'color'>;
|
|
331
331
|
border: TokenReference<'color'>;
|
|
332
332
|
text: TokenReference<'color'>;
|
|
333
|
+
/** Box-shadow for the approval bubble (token ref or raw CSS, e.g. `none`). */
|
|
334
|
+
shadow: string;
|
|
333
335
|
};
|
|
334
336
|
approve: ComponentTokenSet;
|
|
335
337
|
deny: ComponentTokenSet;
|
package/src/types.ts
CHANGED
|
@@ -255,6 +255,12 @@ export type WebMcpConfirmInfo = {
|
|
|
255
255
|
toolName: string;
|
|
256
256
|
args: unknown;
|
|
257
257
|
description?: string;
|
|
258
|
+
/**
|
|
259
|
+
* Display title the tool declared via the WebMCP spec's
|
|
260
|
+
* `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool
|
|
261
|
+
* didn't declare one.
|
|
262
|
+
*/
|
|
263
|
+
title?: string;
|
|
258
264
|
annotations?: {
|
|
259
265
|
readOnlyHint?: boolean;
|
|
260
266
|
untrustedContentHint?: boolean;
|
|
@@ -751,6 +757,31 @@ export type AgentWidgetArtifactsFeature = {
|
|
|
751
757
|
}) => HTMLElement | null;
|
|
752
758
|
};
|
|
753
759
|
|
|
760
|
+
/**
|
|
761
|
+
* How the transcript scrolls while an assistant response streams in.
|
|
762
|
+
*
|
|
763
|
+
* - `"follow"` (default): keep the newest content pinned to the bottom of the
|
|
764
|
+
* viewport, pausing when the user scrolls up and resuming when they return
|
|
765
|
+
* to the bottom.
|
|
766
|
+
* - `"anchor-top"`: on send, scroll the user's message near the top of the
|
|
767
|
+
* viewport and hold it there while the response streams in beneath it
|
|
768
|
+
* (ChatGPT-style). The transcript never auto-scrolls during streaming.
|
|
769
|
+
* - `"none"`: never auto-scroll; the scroll-to-bottom affordance is the only
|
|
770
|
+
* way back to the latest content.
|
|
771
|
+
*/
|
|
772
|
+
export type AgentWidgetScrollMode = "follow" | "anchor-top" | "none";
|
|
773
|
+
|
|
774
|
+
export type AgentWidgetScrollBehaviorFeature = {
|
|
775
|
+
/** Scroll behavior during streamed responses. @default "follow" */
|
|
776
|
+
mode?: AgentWidgetScrollMode;
|
|
777
|
+
/**
|
|
778
|
+
* Gap (px) kept between the anchored user message and the top of the
|
|
779
|
+
* viewport in `"anchor-top"` mode.
|
|
780
|
+
* @default 16
|
|
781
|
+
*/
|
|
782
|
+
anchorTopOffset?: number;
|
|
783
|
+
};
|
|
784
|
+
|
|
754
785
|
export type AgentWidgetScrollToBottomFeature = {
|
|
755
786
|
/**
|
|
756
787
|
* When true, Persona shows a scroll-to-bottom affordance when the user breaks
|
|
@@ -1043,6 +1074,8 @@ export type AgentWidgetFeatureFlags = {
|
|
|
1043
1074
|
composerHistory?: boolean;
|
|
1044
1075
|
/** Shared transcript + event stream scroll-to-bottom affordance. */
|
|
1045
1076
|
scrollToBottom?: AgentWidgetScrollToBottomFeature;
|
|
1077
|
+
/** Transcript scroll behavior during streamed responses. */
|
|
1078
|
+
scrollBehavior?: AgentWidgetScrollBehaviorFeature;
|
|
1046
1079
|
/** Collapsed transcript behavior for tool call rows. */
|
|
1047
1080
|
toolCallDisplay?: AgentWidgetToolCallDisplayFeature;
|
|
1048
1081
|
/** Collapsed transcript behavior for reasoning rows. */
|
|
@@ -1816,6 +1849,23 @@ export interface VoiceProvider {
|
|
|
1816
1849
|
stopPlayback?(): void;
|
|
1817
1850
|
}
|
|
1818
1851
|
|
|
1852
|
+
/**
|
|
1853
|
+
* Extra context for an approval decision, surfaced to `onDecision` and to the
|
|
1854
|
+
* `approve`/`deny` callbacks passed to the `renderApproval` plugin hook.
|
|
1855
|
+
*/
|
|
1856
|
+
export type AgentWidgetApprovalDecisionOptions = {
|
|
1857
|
+
/**
|
|
1858
|
+
* The user chose a "remember this" affordance (e.g. an "Always allow"
|
|
1859
|
+
* button) rather than a one-time decision. The widget resolves the *current*
|
|
1860
|
+
* approval identically whether or not this is set — an approval bubble is a
|
|
1861
|
+
* single binary gate (`approved`/`denied`). Persisting a don't-ask-again
|
|
1862
|
+
* policy for *future* approvals (auto-resolving them, or not surfacing them)
|
|
1863
|
+
* is the integrator's responsibility, typically inside `onDecision`.
|
|
1864
|
+
* Defaults to absent/`false`.
|
|
1865
|
+
*/
|
|
1866
|
+
remember?: boolean;
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1819
1869
|
/**
|
|
1820
1870
|
* Configuration for tool approval bubbles.
|
|
1821
1871
|
* Controls styling, labels, and behavior of the approval UI.
|
|
@@ -1825,6 +1875,8 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1825
1875
|
backgroundColor?: string;
|
|
1826
1876
|
/** Border color of the approval bubble */
|
|
1827
1877
|
borderColor?: string;
|
|
1878
|
+
/** Box-shadow for the approval bubble; pass `"none"` to remove it. Overrides the default `persona-shadow-sm`. */
|
|
1879
|
+
shadow?: string;
|
|
1828
1880
|
/** Color for the title text */
|
|
1829
1881
|
titleColor?: string;
|
|
1830
1882
|
/** Color for the description text */
|
|
@@ -1847,19 +1899,67 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1847
1899
|
approveLabel?: string;
|
|
1848
1900
|
/** Label for the deny button */
|
|
1849
1901
|
denyLabel?: string;
|
|
1902
|
+
/**
|
|
1903
|
+
* Color for the agent-authored reason line (the agent's per-call
|
|
1904
|
+
* justification, shown attributed below the summary when present).
|
|
1905
|
+
*/
|
|
1906
|
+
reasonColor?: string;
|
|
1907
|
+
/**
|
|
1908
|
+
* Label prefix for the agent-authored reason line.
|
|
1909
|
+
* Defaults to "Agent's stated reason:".
|
|
1910
|
+
*/
|
|
1911
|
+
reasonLabel?: string;
|
|
1912
|
+
/**
|
|
1913
|
+
* How the technical details (the tool's agent-facing description and the
|
|
1914
|
+
* raw parameters JSON) are presented:
|
|
1915
|
+
* - `"collapsed"` (default) — hidden behind a "Show details" toggle
|
|
1916
|
+
* - `"expanded"` — visible, with the toggle available to hide them
|
|
1917
|
+
* - `"hidden"` — never rendered
|
|
1918
|
+
*/
|
|
1919
|
+
detailsDisplay?: "collapsed" | "expanded" | "hidden";
|
|
1920
|
+
/** Label for the toggle that reveals the technical details */
|
|
1921
|
+
showDetailsLabel?: string;
|
|
1922
|
+
/** Label for the toggle that hides the technical details */
|
|
1923
|
+
hideDetailsLabel?: string;
|
|
1924
|
+
/**
|
|
1925
|
+
* Build the user-facing summary line for an approval request. Overrides the
|
|
1926
|
+
* default "The assistant wants to use “Tool name”." copy. Return a falsy
|
|
1927
|
+
* value to fall back to the default for that approval. `displayTitle` is
|
|
1928
|
+
* the display name the tool declared via the WebMCP spec's
|
|
1929
|
+
* `ToolDescriptor.title`, when one exists.
|
|
1930
|
+
*/
|
|
1931
|
+
formatDescription?: (approval: {
|
|
1932
|
+
toolName: string;
|
|
1933
|
+
toolType?: string;
|
|
1934
|
+
description: string;
|
|
1935
|
+
parameters?: unknown;
|
|
1936
|
+
displayTitle?: string;
|
|
1937
|
+
/**
|
|
1938
|
+
* Agent-authored justification for this specific call, when the agent
|
|
1939
|
+
* provided one. It is the agent's own claim — if you fold it into the
|
|
1940
|
+
* summary, keep it attributed to the agent.
|
|
1941
|
+
*/
|
|
1942
|
+
reason?: string;
|
|
1943
|
+
}) => string | undefined;
|
|
1850
1944
|
/**
|
|
1851
1945
|
* Custom handler for approval decisions.
|
|
1852
1946
|
* Return void to let the SDK auto-resolve via the API,
|
|
1853
1947
|
* or return a Response/ReadableStream for custom handling.
|
|
1948
|
+
*
|
|
1949
|
+
* `options.remember` is `true` when the decision came from a "remember this"
|
|
1950
|
+
* affordance (e.g. an "Always allow" button in a custom `renderApproval`
|
|
1951
|
+
* plugin). Use it to persist a don't-ask-again policy for future approvals;
|
|
1952
|
+
* the current approval resolves the same way regardless.
|
|
1854
1953
|
*/
|
|
1855
1954
|
onDecision?: (
|
|
1856
1955
|
data: { approvalId: string; executionId: string; agentId: string; toolName: string },
|
|
1857
|
-
decision: 'approved' | 'denied'
|
|
1956
|
+
decision: 'approved' | 'denied',
|
|
1957
|
+
options?: AgentWidgetApprovalDecisionOptions
|
|
1858
1958
|
) => Promise<Response | ReadableStream<Uint8Array> | void>;
|
|
1859
1959
|
};
|
|
1860
1960
|
|
|
1861
1961
|
export type AgentWidgetToolCallConfig = {
|
|
1862
|
-
/** Box-shadow for tool-call bubbles;
|
|
1962
|
+
/** Box-shadow for tool-call bubbles; pass `"none"` to remove it. Overrides the `components.toolBubble.shadow` token / `--persona-tool-bubble-shadow`. */
|
|
1863
1963
|
shadow?: string;
|
|
1864
1964
|
/** Background color of the tool call bubble container. */
|
|
1865
1965
|
backgroundColor?: string;
|
|
@@ -3799,6 +3899,13 @@ export type AgentWidgetApproval = {
|
|
|
3799
3899
|
toolName: string;
|
|
3800
3900
|
toolType?: string;
|
|
3801
3901
|
description: string;
|
|
3902
|
+
/**
|
|
3903
|
+
* Agent-authored justification for this specific call (the agent's own
|
|
3904
|
+
* claim about its intent, extracted server-side from the reserved
|
|
3905
|
+
* `_approvalReason` parameter). Render it attributed to the agent and as
|
|
3906
|
+
* plain text — it is approver context, not a system statement.
|
|
3907
|
+
*/
|
|
3908
|
+
reason?: string;
|
|
3802
3909
|
parameters?: unknown;
|
|
3803
3910
|
resolvedAt?: number;
|
|
3804
3911
|
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
import type { AgentWidgetPlugin } from "./plugins/types";
|
|
7
|
+
|
|
8
|
+
const createMount = () => {
|
|
9
|
+
const mount = document.createElement("div");
|
|
10
|
+
document.body.appendChild(mount);
|
|
11
|
+
return mount;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const injectApproval = (
|
|
15
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
16
|
+
{
|
|
17
|
+
id = "appr-1",
|
|
18
|
+
status = "pending" as "pending" | "approved" | "denied",
|
|
19
|
+
} = {}
|
|
20
|
+
) => {
|
|
21
|
+
controller.injectTestMessage({
|
|
22
|
+
type: "message",
|
|
23
|
+
message: {
|
|
24
|
+
id: `approval-${id}`,
|
|
25
|
+
role: "assistant",
|
|
26
|
+
content: "",
|
|
27
|
+
createdAt: "2026-04-24T00:00:00.000Z",
|
|
28
|
+
streaming: false,
|
|
29
|
+
variant: "approval",
|
|
30
|
+
approval: {
|
|
31
|
+
id,
|
|
32
|
+
status,
|
|
33
|
+
agentId: "agent_1",
|
|
34
|
+
executionId: "exec_1",
|
|
35
|
+
toolName: "Search documentation",
|
|
36
|
+
description: "Search the docs",
|
|
37
|
+
parameters: { query: "approval theming" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const injectNoise = (
|
|
44
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
45
|
+
id = "noise"
|
|
46
|
+
) => {
|
|
47
|
+
controller.injectTestMessage({
|
|
48
|
+
type: "message",
|
|
49
|
+
message: {
|
|
50
|
+
id,
|
|
51
|
+
role: "user",
|
|
52
|
+
content: "noise",
|
|
53
|
+
createdAt: "2026-04-26T00:00:00.000Z",
|
|
54
|
+
streaming: false,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe("renderApproval plugin hook", () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
document.body.innerHTML = "";
|
|
62
|
+
if (typeof localStorage !== "undefined") localStorage.clear();
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders a plugin-returned element in place of the built-in bubble", () => {
|
|
67
|
+
const plugin: AgentWidgetPlugin = {
|
|
68
|
+
id: "custom-approval",
|
|
69
|
+
renderApproval: ({ message }) => {
|
|
70
|
+
const root = document.createElement("div");
|
|
71
|
+
root.setAttribute("data-test-id", "custom-approval");
|
|
72
|
+
root.textContent = message.approval?.toolName ?? "";
|
|
73
|
+
return root;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mount = createMount();
|
|
78
|
+
const controller = createAgentExperience(mount, {
|
|
79
|
+
apiUrl: "https://api.example.com/chat",
|
|
80
|
+
launcher: { enabled: false },
|
|
81
|
+
plugins: [plugin],
|
|
82
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
83
|
+
|
|
84
|
+
injectApproval(controller);
|
|
85
|
+
|
|
86
|
+
const custom = mount.querySelector('[data-test-id="custom-approval"]');
|
|
87
|
+
expect(custom).not.toBeNull();
|
|
88
|
+
expect(custom?.textContent).toBe("Search documentation");
|
|
89
|
+
// The built-in bubble must NOT also render.
|
|
90
|
+
expect(mount.querySelector(".persona-approval-bubble")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("preserves plugin click listeners (e.g. an accordion) across morph re-renders", () => {
|
|
94
|
+
const toggleSpy = vi.fn();
|
|
95
|
+
const plugin: AgentWidgetPlugin = {
|
|
96
|
+
id: "accordion-approval",
|
|
97
|
+
renderApproval: ({ message }) => {
|
|
98
|
+
if (message.approval?.status !== "pending") return null;
|
|
99
|
+
const root = document.createElement("div");
|
|
100
|
+
root.setAttribute("data-test-id", "appr-root");
|
|
101
|
+
const head = document.createElement("button");
|
|
102
|
+
head.type = "button";
|
|
103
|
+
head.setAttribute("data-test-id", "appr-head");
|
|
104
|
+
head.setAttribute("data-action", "toggle");
|
|
105
|
+
const params = document.createElement("pre");
|
|
106
|
+
params.setAttribute("data-test-id", "appr-params");
|
|
107
|
+
// Single delegated listener at the root — the recommended pattern.
|
|
108
|
+
root.addEventListener("click", (e) => {
|
|
109
|
+
const target = (e.target as HTMLElement).closest("[data-action]");
|
|
110
|
+
if (target?.getAttribute("data-action") === "toggle") {
|
|
111
|
+
params.hidden = !params.hidden;
|
|
112
|
+
toggleSpy();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
root.append(head, params);
|
|
116
|
+
return root;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const mount = createMount();
|
|
121
|
+
const controller = createAgentExperience(mount, {
|
|
122
|
+
apiUrl: "https://api.example.com/chat",
|
|
123
|
+
launcher: { enabled: false },
|
|
124
|
+
plugins: [plugin],
|
|
125
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
126
|
+
|
|
127
|
+
injectApproval(controller);
|
|
128
|
+
// Force a re-render — the scenario where innerHTML morph drops listeners
|
|
129
|
+
// on a freshly-built plugin element unless it's hydrated post-morph.
|
|
130
|
+
injectNoise(controller);
|
|
131
|
+
|
|
132
|
+
const head = mount.querySelector<HTMLButtonElement>('[data-test-id="appr-head"]');
|
|
133
|
+
const params = mount.querySelector<HTMLPreElement>('[data-test-id="appr-params"]');
|
|
134
|
+
expect(head).not.toBeNull();
|
|
135
|
+
expect(params).not.toBeNull();
|
|
136
|
+
|
|
137
|
+
head!.click();
|
|
138
|
+
expect(toggleSpy).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(params!.hidden).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("resolves via the approve callback (with the remember option)", async () => {
|
|
143
|
+
const onDecision = vi.fn(async () => {});
|
|
144
|
+
const plugin: AgentWidgetPlugin = {
|
|
145
|
+
id: "approve-callback",
|
|
146
|
+
renderApproval: ({ message, approve }) => {
|
|
147
|
+
if (message.approval?.status !== "pending") return null;
|
|
148
|
+
const root = document.createElement("div");
|
|
149
|
+
const btn = document.createElement("button");
|
|
150
|
+
btn.setAttribute("data-test-id", "always-allow");
|
|
151
|
+
btn.addEventListener("click", () => approve({ remember: true }));
|
|
152
|
+
root.appendChild(btn);
|
|
153
|
+
return root;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const mount = createMount();
|
|
158
|
+
const controller = createAgentExperience(mount, {
|
|
159
|
+
apiUrl: "https://api.example.com/chat",
|
|
160
|
+
launcher: { enabled: false },
|
|
161
|
+
approval: { onDecision },
|
|
162
|
+
plugins: [plugin],
|
|
163
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
164
|
+
|
|
165
|
+
injectApproval(controller);
|
|
166
|
+
injectNoise(controller);
|
|
167
|
+
|
|
168
|
+
const btn = mount.querySelector<HTMLButtonElement>('[data-test-id="always-allow"]');
|
|
169
|
+
expect(btn).not.toBeNull();
|
|
170
|
+
btn!.click();
|
|
171
|
+
|
|
172
|
+
await Promise.resolve();
|
|
173
|
+
expect(onDecision).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({ toolName: "Search documentation" }),
|
|
175
|
+
"approved",
|
|
176
|
+
{ remember: true }
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("falls through to the built-in bubble when the plugin returns null (resolved state)", () => {
|
|
181
|
+
const plugin: AgentWidgetPlugin = {
|
|
182
|
+
id: "pending-only-approval",
|
|
183
|
+
renderApproval: ({ message }) => {
|
|
184
|
+
if (message.approval?.status !== "pending") return null;
|
|
185
|
+
const root = document.createElement("div");
|
|
186
|
+
root.setAttribute("data-test-id", "custom-pending");
|
|
187
|
+
return root;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const mount = createMount();
|
|
192
|
+
const controller = createAgentExperience(mount, {
|
|
193
|
+
apiUrl: "https://api.example.com/chat",
|
|
194
|
+
launcher: { enabled: false },
|
|
195
|
+
plugins: [plugin],
|
|
196
|
+
} as unknown as Parameters<typeof createAgentExperience>[1]);
|
|
197
|
+
|
|
198
|
+
// Inject directly in the resolved state — plugin opts out, built-in renders.
|
|
199
|
+
injectApproval(controller, { status: "approved" });
|
|
200
|
+
|
|
201
|
+
expect(mount.querySelector('[data-test-id="custom-pending"]')).toBeNull();
|
|
202
|
+
expect(mount.querySelector(".persona-approval-bubble")).not.toBeNull();
|
|
203
|
+
});
|
|
204
|
+
});
|