@runtypelabs/persona 4.14.0 → 4.15.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 (81) hide show
  1. package/README.md +4 -4
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
  5. package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-QJ4LV2KW.js +1 -0
  9. package/dist/codegen.cjs +12 -11
  10. package/dist/codegen.js +14 -13
  11. package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
  12. package/dist/context-mentions.cjs +11 -8
  13. package/dist/context-mentions.d.cts +241 -16
  14. package/dist/context-mentions.d.ts +241 -16
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +67 -66
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +301 -41
  19. package/dist/index.d.ts +301 -41
  20. package/dist/index.global.js +59 -56
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +67 -66
  23. package/dist/index.js.map +1 -1
  24. package/dist/launcher.global.js +5 -2
  25. package/dist/launcher.global.js.map +1 -1
  26. package/dist/smart-dom-reader.d.cts +241 -16
  27. package/dist/smart-dom-reader.d.ts +241 -16
  28. package/dist/theme-editor-preview.cjs +66 -66
  29. package/dist/theme-editor-preview.d.cts +255 -17
  30. package/dist/theme-editor-preview.d.ts +255 -17
  31. package/dist/theme-editor-preview.js +56 -56
  32. package/dist/theme-editor.cjs +5 -5
  33. package/dist/theme-editor.d.cts +242 -17
  34. package/dist/theme-editor.d.ts +242 -17
  35. package/dist/theme-editor.js +7 -7
  36. package/dist/theme-reference.cjs +1 -1
  37. package/dist/theme-reference.d.cts +38 -0
  38. package/dist/theme-reference.d.ts +38 -0
  39. package/dist/theme-reference.js +1 -1
  40. package/dist/widget.css +1 -1
  41. package/package.json +1 -1
  42. package/src/ask-user-question-tool.ts +10 -7
  43. package/src/components/artifact-preview.ts +1 -1
  44. package/src/components/composer-parts.test.ts +103 -4
  45. package/src/components/composer-parts.ts +53 -15
  46. package/src/components/messages.ts +5 -2
  47. package/src/components/panel.ts +31 -5
  48. package/src/components/pill-composer-builder.test.ts +18 -0
  49. package/src/components/suggestions.test.ts +233 -0
  50. package/src/components/suggestions.ts +463 -81
  51. package/src/components/widget-view.ts +4 -0
  52. package/src/defaults.ts +5 -3
  53. package/src/generated/runtype-openapi-contract.ts +17 -0
  54. package/src/index-core.ts +18 -2
  55. package/src/launcher-global.ts +1 -1
  56. package/src/plugins/types.ts +71 -3
  57. package/src/presets.ts +3 -3
  58. package/src/session.ts +10 -8
  59. package/src/styles/widget-styles.test.ts +162 -0
  60. package/src/styles/widget.css +430 -0
  61. package/src/suggest-replies-tool.test.ts +233 -2
  62. package/src/suggest-replies-tool.ts +115 -39
  63. package/src/theme-editor/preview-utils.test.ts +34 -0
  64. package/src/theme-editor/preview-utils.ts +77 -2
  65. package/src/theme-editor/sections.test.ts +33 -0
  66. package/src/theme-editor/sections.ts +53 -5
  67. package/src/theme-reference.ts +9 -2
  68. package/src/types/theme.ts +32 -0
  69. package/src/types.ts +152 -16
  70. package/src/ui.detached-panel.test.ts +1 -1
  71. package/src/ui.suggest-replies.test.ts +1066 -1
  72. package/src/ui.ts +268 -20
  73. package/src/utils/artifact-file.test.ts +1 -1
  74. package/src/utils/code-generators.test.ts +40 -0
  75. package/src/utils/code-generators.ts +22 -0
  76. package/src/utils/config-merge.test.ts +24 -0
  77. package/src/utils/icons.test.ts +26 -0
  78. package/src/utils/icons.ts +8 -0
  79. package/src/utils/theme.test.ts +79 -0
  80. package/src/utils/tokens.ts +64 -0
  81. package/dist/chunk-MMUPR2JW.js +0 -1
package/src/index-core.ts CHANGED
@@ -140,7 +140,18 @@ export type {
140
140
  AskUserQuestionPrompt,
141
141
  AskUserQuestionOption,
142
142
  AgentWidgetAskUserQuestionFeature,
143
- AgentWidgetAskUserQuestionStyles
143
+ AgentWidgetAskUserQuestionStyles,
144
+ AgentWidgetSuggestion,
145
+ AgentWidgetSuggestionChipsConfig,
146
+ AgentWidgetResolvedSuggestion,
147
+ AgentWidgetSuggestionBehavior,
148
+ AgentWidgetSuggestionVariant,
149
+ AgentWidgetSuggestionSurface,
150
+ AgentWidgetSuggestionSource,
151
+ AgentWidgetSuggestionSurfaceConfig,
152
+ AgentWidgetStarterSuggestionsConfig,
153
+ AgentWidgetFollowUpSuggestionsConfig,
154
+ AgentWidgetSuggestionsConfig
144
155
  } from "./types";
145
156
 
146
157
  export {
@@ -326,7 +337,12 @@ export { VERSION } from "./version";
326
337
  export type { AgentWidgetInitHandle };
327
338
 
328
339
  // Plugin system exports
329
- export type { AgentWidgetPlugin } from "./plugins/types";
340
+ export type {
341
+ AgentWidgetPlugin,
342
+ AgentWidgetTransformSuggestionsContext,
343
+ AgentWidgetRenderSuggestionContext,
344
+ AgentWidgetSuggestionSelectContext
345
+ } from "./plugins/types";
330
346
  export { pluginRegistry } from "./plugins/registry";
331
347
 
332
348
  // Stream animation plugin API: lets consumers register custom animations
@@ -76,7 +76,7 @@ const mergeCriticalLauncherConfig = (
76
76
  * Mirrors the full widget's DOM exactly (`runtime/init.ts` + `ui.ts`): a
77
77
  * `[data-persona-root]` wrapper carries the theme CSS variables and the launcher
78
78
  * button is its child. Keeping the theme vars on the wrapper (not the button)
79
- * leaves the button's own inline style byte-identical to the full widget's, so
79
+ * leaves the button's own inline style matching the full widget's, so
80
80
  * the eventual mount-then-remove handoff is invisible.
81
81
  */
82
82
  export const mount = (
@@ -8,9 +8,51 @@ import {
8
8
  EventStreamViewRenderContext,
9
9
  EventStreamRowRenderContext,
10
10
  EventStreamToolbarRenderContext,
11
- EventStreamPayloadRenderContext
11
+ EventStreamPayloadRenderContext,
12
+ AgentWidgetSuggestion,
13
+ AgentWidgetResolvedSuggestion,
14
+ AgentWidgetSuggestionSource,
15
+ AgentWidgetSuggestionSurface,
16
+ AgentWidgetSuggestionVariant
12
17
  } from "../types";
13
18
 
19
+ export type AgentWidgetTransformSuggestionsContext = {
20
+ /**
21
+ * Fresh copy of the suggestions entering this plugin, already normalized:
22
+ * shorthand strings expanded and the effective send/fill behavior resolved.
23
+ */
24
+ suggestions: AgentWidgetResolvedSuggestion[];
25
+ surface: AgentWidgetSuggestionSurface;
26
+ source: AgentWidgetSuggestionSource;
27
+ config: AgentWidgetConfig;
28
+ };
29
+
30
+ export type AgentWidgetRenderSuggestionContext = {
31
+ /** Fully normalized suggestion, including the effective send/fill behavior. */
32
+ suggestion: AgentWidgetResolvedSuggestion;
33
+ index: number;
34
+ surface: AgentWidgetSuggestionSurface;
35
+ source: AgentWidgetSuggestionSource;
36
+ variant: AgentWidgetSuggestionVariant;
37
+ streaming: boolean;
38
+ config: AgentWidgetConfig;
39
+ /** Build Persona's standard chip, card, or list item. */
40
+ defaultRenderer: () => HTMLElement;
41
+ /**
42
+ * Run the normal selection pipeline: DOM event, plugin selection hooks, then
43
+ * send or fill. Custom renderers should call this instead of sending directly.
44
+ */
45
+ select: () => void;
46
+ };
47
+
48
+ export type AgentWidgetSuggestionSelectContext = {
49
+ suggestion: AgentWidgetResolvedSuggestion;
50
+ surface: AgentWidgetSuggestionSurface;
51
+ source: AgentWidgetSuggestionSource;
52
+ variant: AgentWidgetSuggestionVariant;
53
+ config: AgentWidgetConfig;
54
+ };
55
+
14
56
  /**
15
57
  * Plugin interface for customizing widget components
16
58
  */
@@ -25,6 +67,34 @@ export interface AgentWidgetPlugin {
25
67
  */
26
68
  priority?: number;
27
69
 
70
+ /**
71
+ * Transform, filter, enrich, or reorder suggestions. Hooks receive normalized
72
+ * items and compose in plugin priority order; the return value may use the
73
+ * loose shape (string shorthand included) and is re-normalized before the next
74
+ * hook. A per-item `behavior` set here survives. `maxItems` caps the list
75
+ * after the full chain. Return an empty array to hide the surface.
76
+ */
77
+ transformSuggestions?: (
78
+ context: AgentWidgetTransformSuggestionsContext
79
+ ) => AgentWidgetSuggestion[];
80
+
81
+ /**
82
+ * Render one starter or follow-up suggestion. Return null to use Persona's
83
+ * default chip/card/list renderer. Call `select()` from custom interaction
84
+ * controls to preserve events, selection hooks, and send/fill behavior.
85
+ */
86
+ renderSuggestion?: (
87
+ context: AgentWidgetRenderSuggestionContext
88
+ ) => HTMLElement | null;
89
+
90
+ /**
91
+ * Observe or intercept a suggestion selection. Return false to cancel the
92
+ * built-in send/fill action. Hooks run in plugin priority order.
93
+ */
94
+ onSuggestionSelect?: (
95
+ context: AgentWidgetSuggestionSelectContext
96
+ ) => void | boolean;
97
+
28
98
  /**
29
99
  * Custom renderer for message bubbles
30
100
  * Return null to use default renderer
@@ -298,5 +368,3 @@ export interface AgentWidgetPlugin {
298
368
 
299
369
 
300
370
 
301
-
302
-
package/src/presets.ts CHANGED
@@ -75,9 +75,9 @@ export const PRESET_SHOP: WidgetPreset = {
75
75
  sendButtonLabel: "Send",
76
76
  },
77
77
  suggestionChips: [
78
- "What can you help me with?",
79
- "Tell me about your features",
80
- "How does this work?",
78
+ "Show me what you can help with",
79
+ "Walk me through getting started",
80
+ "Answer a question about this page",
81
81
  ],
82
82
  },
83
83
  };
package/src/session.ts CHANGED
@@ -2,6 +2,7 @@ import { AgentWidgetClient, type SSEEventCallback } from "./client";
2
2
  import { isWebMcpToolName } from "./webmcp-bridge";
3
3
  import {
4
4
  SUGGEST_REPLIES_TOOL_NAME,
5
+ resolveFollowUpsFeature,
5
6
  suggestRepliesToolResult,
6
7
  } from "./suggest-replies-tool";
7
8
  import {
@@ -77,10 +78,11 @@ export type AgentWidgetSessionStatus =
77
78
  * request. When NONE of these change, `updateConfig` can refresh the live
78
79
  * client in place (preserving the WebMCP bridge and any in-flight stream/resume)
79
80
  * instead of swapping in a fresh client and tearing down WebMCP state. Fields
80
- * outside this list (theme, copy, layout, suggestionChips, iterationDisplay,
81
- * postprocessMessage, feature display toggles, …) are display-only and safe to
82
- * apply mid-turn: which is what a self-styling widget needs when a `webmcp:*`
83
- * theme tool re-renders the widget while the agent's turn is still streaming.
81
+ * outside this list (theme, copy, layout, suggestions, suggestionChips,
82
+ * iterationDisplay, postprocessMessage, feature display toggles, …) are
83
+ * display-only and safe to apply mid-turn: which is what a self-styling widget
84
+ * needs when a `webmcp:*` theme tool re-renders the widget while the agent's
85
+ * turn is still streaming.
84
86
  *
85
87
  * Compared by identity (`!==`): primitives by value, functions/objects by
86
88
  * reference. A consumer that rebuilds these objects on every render simply
@@ -2983,15 +2985,15 @@ export class AgentWidgetSession {
2983
2985
  // indefinitely: `resolveWebMcpToolCall` translates the missing-bridge
2984
2986
  // case into an isError result that resumes the flow cleanly.
2985
2987
  // `suggest_replies`, by contrast, is gated on its feature flag: when
2986
- // `features.suggestReplies.enabled === false` the widget neither
2987
- // renders chips nor resumes: the same parked-execution posture as a
2988
- // server-declared ask_user_question with its sheet disabled.
2988
+ // the resolved `enabled` is false the widget neither renders chips nor
2989
+ // resumes: the same parked-execution posture as a server-declared
2990
+ // ask_user_question with its sheet disabled.
2989
2991
  const tc = event.message.toolCall;
2990
2992
  const autoResolvable =
2991
2993
  !!tc?.name &&
2992
2994
  (isWebMcpToolName(tc.name) ||
2993
2995
  (tc.name === SUGGEST_REPLIES_TOOL_NAME &&
2994
- this.config.features?.suggestReplies?.enabled !== false));
2996
+ resolveFollowUpsFeature(this.config).enabled));
2995
2997
  if (
2996
2998
  event.message.agentMetadata?.awaitingLocalTool === true &&
2997
2999
  autoResolvable
@@ -0,0 +1,162 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const widgetCss = readFileSync(
6
+ new URL("./widget.css", import.meta.url),
7
+ "utf8",
8
+ );
9
+
10
+ describe("suggestion interaction styles", () => {
11
+ it("keeps suggestion surfaces from collapsing inside constrained transcripts", () => {
12
+ const selector = ".persona-suggestions {";
13
+ const start = widgetCss.indexOf(selector);
14
+ const end = widgetCss.indexOf("\n}", start);
15
+ const surfaceRule = widgetCss.slice(start, end);
16
+
17
+ expect(start).toBeGreaterThan(-1);
18
+ expect(surfaceRule).toContain("flex-shrink: 0");
19
+ });
20
+
21
+ it("keeps the primary emphasis a quiet accent rather than a solid fill", () => {
22
+ const selector = '.persona-suggestion[data-emphasis="primary"] {';
23
+ const start = widgetCss.indexOf(selector);
24
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
25
+
26
+ expect(start).toBeGreaterThan(-1);
27
+ expect(rule).toContain("border-color: var(--persona-primary)");
28
+ // Plain declaration first so engines without color-mix stay transparent.
29
+ expect(rule).toContain("background: transparent");
30
+ expect(rule).toContain(
31
+ "background: color-mix(in srgb, var(--persona-primary) 6%, transparent)",
32
+ );
33
+ expect(rule).not.toContain("--persona-text-inverse");
34
+ });
35
+
36
+ it("strengthens the primary wash on hover instead of filling it", () => {
37
+ const selector =
38
+ '.persona-suggestion[data-emphasis="primary"]:hover {';
39
+ const start = widgetCss.indexOf(selector);
40
+ const hoverRule = widgetCss.slice(start, widgetCss.indexOf("\n }", start));
41
+
42
+ expect(start).toBeGreaterThan(-1);
43
+ expect(hoverRule).toContain("border-color: var(--persona-primary)");
44
+ expect(hoverRule).toContain(
45
+ "color-mix(in srgb, var(--persona-primary) 12%, transparent)",
46
+ );
47
+ expect(hoverRule).not.toContain("filter: brightness");
48
+ });
49
+
50
+ it("rests cards flat and reserves the shadow plus lift for hover", () => {
51
+ const restStart = widgetCss.indexOf(".persona-suggestion--card {");
52
+ const restRule = widgetCss.slice(
53
+ restStart,
54
+ widgetCss.indexOf("\n}", restStart),
55
+ );
56
+
57
+ expect(restStart).toBeGreaterThan(-1);
58
+ expect(restRule).toContain(
59
+ "box-shadow: var(--persona-components-suggestion-card-shadow, none)",
60
+ );
61
+
62
+ const hoverStart = widgetCss.indexOf(".persona-suggestion--card:hover {");
63
+ const hoverRule = widgetCss.slice(
64
+ hoverStart,
65
+ widgetCss.indexOf("\n }", hoverStart),
66
+ );
67
+
68
+ expect(hoverStart).toBeGreaterThan(-1);
69
+ expect(hoverRule).toContain("box-shadow: var(--persona-shadow-sm)");
70
+ expect(hoverRule).toContain("transform: translateY(-1px)");
71
+ });
72
+
73
+ it("reveals the suggestion arrow only on hover and keyboard focus", () => {
74
+ // Searched past `__copy`: the bare selector also appears in the shared
75
+ // `flex: 0 0 auto` group above it and the coarse-pointer override below.
76
+ const restStart = widgetCss.indexOf(
77
+ ".persona-suggestion__arrow {",
78
+ widgetCss.indexOf(".persona-suggestion__copy {"),
79
+ );
80
+ const restRule = widgetCss.slice(
81
+ restStart,
82
+ widgetCss.indexOf("\n}", restStart),
83
+ );
84
+
85
+ expect(restStart).toBeGreaterThan(-1);
86
+ expect(restRule).toContain("opacity: 0;");
87
+ expect(restRule).toContain("transition: opacity");
88
+ expect(widgetCss).toContain(
89
+ ".persona-suggestion:focus-visible .persona-suggestion__arrow",
90
+ );
91
+ expect(widgetCss).toContain(
92
+ ".persona-suggestion:hover .persona-suggestion__arrow",
93
+ );
94
+ });
95
+
96
+ it("caps the starter card grid so wide panels get two columns", () => {
97
+ expect(widgetCss).toContain(
98
+ "grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr))",
99
+ );
100
+ const selector =
101
+ '.persona-suggestions[data-variant="card"][data-persona-suggestion-surface="starter"] {';
102
+ const start = widgetCss.indexOf(selector);
103
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
104
+
105
+ expect(start).toBeGreaterThan(-1);
106
+ expect(rule).toContain("max-width: 600px");
107
+ });
108
+
109
+ it("masks only the scrollable chip edges that still contain content", () => {
110
+ expect(widgetCss).toContain(
111
+ '[data-overflow="scroll"][data-scroll-right]:not(',
112
+ );
113
+ expect(widgetCss).toContain(
114
+ '[data-overflow="scroll"][data-scroll-left]:not(',
115
+ );
116
+ expect(widgetCss).toContain(
117
+ '[data-overflow="scroll"][data-scroll-left][data-scroll-right]',
118
+ );
119
+ expect(widgetCss).toContain(
120
+ "--persona-suggestion-scroll-fade-size: 32px",
121
+ );
122
+ });
123
+ });
124
+
125
+ describe("composer spacing styles", () => {
126
+ it("themes the composer form padding and gap with utility-matching fallbacks", () => {
127
+ const selector =
128
+ "[data-persona-root] .persona-widget-composer:not(.persona-pill-composer) {";
129
+ const start = widgetCss.indexOf(selector);
130
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
131
+
132
+ expect(start).toBeGreaterThan(-1);
133
+ // Fallbacks reproduce `persona-px-4 persona-py-3` and `persona-gap-2`.
134
+ expect(rule).toContain(
135
+ "padding: var(--persona-composer-padding, 0.75rem 1rem)",
136
+ );
137
+ expect(rule).toContain("gap: var(--persona-composer-gap, 0.5rem)");
138
+ });
139
+
140
+ it("themes the composer textarea type with text-sm fallbacks", () => {
141
+ const selector = "[data-persona-root] .persona-composer-textarea {";
142
+ const start = widgetCss.indexOf(selector);
143
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
144
+
145
+ expect(start).toBeGreaterThan(-1);
146
+ expect(rule).toContain(
147
+ "font-size: var(--persona-composer-font-size, 0.875rem)",
148
+ );
149
+ expect(rule).toContain(
150
+ "line-height: var(--persona-composer-line-height, 1.25rem)",
151
+ );
152
+ });
153
+
154
+ it("leaves the pill composer on its own single-row geometry", () => {
155
+ const start = widgetCss.indexOf(".persona-pill-composer {");
156
+ const rule = widgetCss.slice(start, widgetCss.indexOf("\n}", start));
157
+
158
+ expect(start).toBeGreaterThan(-1);
159
+ expect(rule).toContain("padding: 6px 14px");
160
+ expect(rule).toContain("gap: 8px");
161
+ });
162
+ });