@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
@@ -2,14 +2,13 @@
2
2
  * Built-in `suggest_replies` client tool.
3
3
  *
4
4
  * The widget can advertise this tool to the agent on every dispatch via
5
- * `clientTools[]` (set `features.suggestReplies.expose: true`): the same
5
+ * `clientTools[]` (set `suggestions.followUps.expose: true`): the same
6
6
  * wire surface as `ask_user_question` and WebMCP page tools. When the model
7
- * calls it, the execution pauses with a `step_await`
8
- * (`awaitReason: "local_tool_required"`); unlike `ask_user_question`, the
9
- * widget resolves it FIRE-AND-FORGET: it renders the suggestions as tappable
10
- * chips above the composer and immediately resumes the execution with a
11
- * canned "shown" result, so the agent's turn completes without waiting on the
12
- * user. Tapping a chip sends its text verbatim as the user's next message.
7
+ * calls it, the execution pauses with a `step_await` (a local-tool pause
8
+ * carries no `awaitReason`; that discriminator is internal). Unlike
9
+ * `ask_user_question`, the widget resolves it FIRE-AND-FORGET: it renders the
10
+ * configured follow-up surface and immediately resumes the execution with a
11
+ * canned "shown" result, so the turn completes without waiting on the user.
13
12
  *
14
13
  * Chip visibility is DERIVED state, not imperative show/hide: the chips of
15
14
  * the last `suggest_replies` tool message with no user message after it are
@@ -21,6 +20,7 @@
21
20
  import type {
22
21
  AgentWidgetConfig,
23
22
  AgentWidgetMessage,
23
+ AgentWidgetSuggestion,
24
24
  ClientToolDefinition,
25
25
  } from "./types";
26
26
 
@@ -30,9 +30,10 @@ export const SUGGEST_REPLIES_TOOL_NAME = "suggest_replies";
30
30
  export const SUGGEST_REPLIES_MAX = 4;
31
31
 
32
32
  /**
33
- * JSON Schema for the tool's parameters. Mirrors what
34
- * {@link parseSuggestRepliesPayload} hydrates the chips from, so the schema
35
- * the model is held to and the shape the renderer expects can never drift.
33
+ * JSON Schema for the tool's parameters. The model owns semantics only: id,
34
+ * icon, behavior, and emphasis are presentation, and hosts set them with the
35
+ * `transformSuggestions` plugin hook. A single object type is used because
36
+ * strict structured-output modes reject or flatten `oneOf`.
36
37
  */
37
38
  export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
38
39
  type: "object",
@@ -43,7 +44,16 @@ export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
43
44
  maxItems: SUGGEST_REPLIES_MAX,
44
45
  description:
45
46
  "1-4 short, distinct follow-up replies, phrased in the user's voice.",
46
- items: { type: "string", minLength: 1, maxLength: 60 },
47
+ items: {
48
+ type: "object",
49
+ properties: {
50
+ label: { type: "string", minLength: 1, maxLength: 80 },
51
+ prompt: { type: "string", minLength: 1, maxLength: 500 },
52
+ description: { type: "string", minLength: 1, maxLength: 160 },
53
+ },
54
+ required: ["label"],
55
+ additionalProperties: false,
56
+ },
47
57
  },
48
58
  },
49
59
  required: ["suggestions"],
@@ -52,7 +62,7 @@ export const SUGGEST_REPLIES_PARAMETERS_SCHEMA = {
52
62
 
53
63
  /**
54
64
  * The `ClientToolDefinition` shipped on `dispatch.clientTools[]` when
55
- * `features.suggestReplies.expose` is on. Exported so integrators who prefer
65
+ * `suggestions.followUps.expose` is on. Exported so integrators who prefer
56
66
  * declaring the tool server-side (a flow's `runtimeTools`) can reuse the same
57
67
  * description and schema instead of hand-writing them.
58
68
  */
@@ -60,12 +70,15 @@ export const SUGGEST_REPLIES_CLIENT_TOOL: ClientToolDefinition = {
60
70
  name: SUGGEST_REPLIES_TOOL_NAME,
61
71
  description:
62
72
  "Offer the user tappable quick-reply suggestions for their next message. " +
63
- "Call at most once per turn, as the LAST action after your reply text is " +
64
- "complete. Each suggestion is sent verbatim as the user's next message, " +
65
- 'so phrase suggestions in the user\'s voice (e.g. "Tell me more about ' +
66
- 'pricing"). Keep them short and distinct. The result only confirms the ' +
67
- "suggestions were shown: do not add further commentary after calling " +
68
- "this tool; end your turn.",
73
+ "Call at most once per turn, as the last action after your reply text is " +
74
+ "complete. Give each suggestion a `label` with the short visible text, an " +
75
+ "optional `prompt` with the fuller text placed as the user's message " +
76
+ "(defaults to the label), and an optional `description` with one line of " +
77
+ "supporting copy. Suggestions are sent as the user's next message, so " +
78
+ 'phrase prompts in the user\'s voice (e.g. "Tell me more about pricing"). ' +
79
+ "Keep them short and distinct. The result only confirms the suggestions " +
80
+ "were shown: do not add further commentary after calling this tool; end " +
81
+ "your turn.",
69
82
  parametersSchema: SUGGEST_REPLIES_PARAMETERS_SCHEMA,
70
83
  origin: "sdk",
71
84
  annotations: { readOnlyHint: true },
@@ -90,11 +103,15 @@ export const isSuggestRepliesMessage = (
90
103
  message.toolCall?.name === SUGGEST_REPLIES_TOOL_NAME;
91
104
 
92
105
  /**
93
- * Tolerant parse of a `suggest_replies` tool call's args into chip labels:
94
- * accepts a JSON string or object, coerces items to trimmed strings, drops
95
- * empties, and truncates past the renderer cap with a console warning.
106
+ * Tolerant parse of a `suggest_replies` tool call's args into suggestion
107
+ * items. Plain strings stay strings for older flows that emit
108
+ * `suggestions: string[]`; objects keep only label, prompt, and description,
109
+ * so a model that ignores `additionalProperties` cannot set presentation.
110
+ * Hosts add id, icon, behavior, and emphasis via `transformSuggestions`.
96
111
  */
97
- export const parseSuggestRepliesPayload = (args: unknown): string[] => {
112
+ export const parseSuggestRepliesPayload = (
113
+ args: unknown
114
+ ): AgentWidgetSuggestion[] => {
98
115
  let parsed: unknown = args;
99
116
  if (typeof parsed === "string") {
100
117
  try {
@@ -106,17 +123,32 @@ export const parseSuggestRepliesPayload = (args: unknown): string[] => {
106
123
  const raw = (parsed as { suggestions?: unknown } | null | undefined)
107
124
  ?.suggestions;
108
125
  if (!Array.isArray(raw)) return [];
109
- const chips = raw
110
- .filter((item): item is string => typeof item === "string")
111
- .map((item) => item.trim())
112
- .filter((item) => item.length > 0);
113
- if (chips.length > SUGGEST_REPLIES_MAX) {
126
+ const suggestions = raw.flatMap<AgentWidgetSuggestion>((item) => {
127
+ if (typeof item === "string") {
128
+ const value = item.trim();
129
+ return value ? [value] : [];
130
+ }
131
+ if (!item || typeof item !== "object") return [];
132
+ const value = item as Record<string, unknown>;
133
+ if (typeof value.label !== "string" || !value.label.trim()) return [];
134
+ const suggestion: Exclude<AgentWidgetSuggestion, string> = {
135
+ label: value.label.trim(),
136
+ };
137
+ if (typeof value.prompt === "string" && value.prompt.trim()) {
138
+ suggestion.prompt = value.prompt.trim();
139
+ }
140
+ if (typeof value.description === "string" && value.description.trim()) {
141
+ suggestion.description = value.description.trim();
142
+ }
143
+ return [suggestion];
144
+ });
145
+ if (suggestions.length > SUGGEST_REPLIES_MAX) {
114
146
  console.warn(
115
- `[persona] suggest_replies: ${chips.length} suggestions exceeds the cap of ${SUGGEST_REPLIES_MAX}; extra suggestions dropped.`,
147
+ `[persona] suggest_replies: ${suggestions.length} suggestions exceeds the cap of ${SUGGEST_REPLIES_MAX}; extra suggestions dropped.`,
116
148
  );
117
- return chips.slice(0, SUGGEST_REPLIES_MAX);
149
+ return suggestions.slice(0, SUGGEST_REPLIES_MAX);
118
150
  }
119
- return chips;
151
+ return suggestions;
120
152
  };
121
153
 
122
154
  /**
@@ -127,7 +159,7 @@ export const parseSuggestRepliesPayload = (args: unknown): string[] => {
127
159
  */
128
160
  export const latestAgentSuggestions = (
129
161
  messages: AgentWidgetMessage[],
130
- ): string[] | null => {
162
+ ): AgentWidgetSuggestion[] | null => {
131
163
  for (let i = messages.length - 1; i >= 0; i--) {
132
164
  const message = messages[i];
133
165
  if (message.role === "user") return null;
@@ -138,15 +170,59 @@ export const latestAgentSuggestions = (
138
170
  return null;
139
171
  };
140
172
 
173
+ export type ResolvedFollowUpsFeature = {
174
+ /** Widget renders follow-up chips and auto-resumes the tool call. */
175
+ enabled: boolean;
176
+ /** Tool ships on `dispatch.clientTools[]`. */
177
+ expose: boolean;
178
+ };
179
+
180
+ // Keyed on the conflict itself, not the config object: `controller.update()`
181
+ // rebuilds the config on every call, so an object-keyed dedupe re-warns
182
+ // forever. Trade-off: two widgets with the identical conflict warn once total
183
+ // per page load.
184
+ const conflictWarnings = new Set<string>();
185
+
186
+ /** Always warns, debug or not: a config conflict is a developer error. */
187
+ const warnConflict = (
188
+ key: "enabled" | "expose",
189
+ legacyValue: boolean,
190
+ newValue: boolean,
191
+ ): void => {
192
+ const signature = `${key}:${legacyValue}:${newValue}`;
193
+ if (conflictWarnings.has(signature)) return;
194
+ conflictWarnings.add(signature);
195
+ console.warn(
196
+ `[persona] suggestions.followUps.${key} and features.suggestReplies.${key} disagree; suggestions.followUps.${key} wins.`,
197
+ );
198
+ };
199
+
141
200
  /**
142
- * Gate for advertising the tool: `expose` opts it into the agent's catalog,
143
- * and `enabled !== false` guarantees the widget will actually auto-resolve
144
- * and render chips for it: exposing the tool with the feature disabled
145
- * would park the execution on a generic tool bubble with no resume coming.
201
+ * Single source of truth for the follow-ups capability flags. Resolution is
202
+ * per key so a `features.suggestReplies` embed that adds presentation-only
203
+ * `suggestions.followUps` keys is not silently re-enabled. `expose` is forced
204
+ * off when `enabled` resolves false: advertising the tool without the widget
205
+ * auto-resolving it parks the execution on a generic tool bubble.
146
206
  */
147
- export const shouldExposeSuggestReplies = (
207
+ export const resolveFollowUpsFeature = (
148
208
  config: AgentWidgetConfig | undefined,
149
- ): boolean => {
150
- const feature = config?.features?.suggestReplies;
151
- return feature?.expose === true && feature.enabled !== false;
209
+ ): ResolvedFollowUpsFeature => {
210
+ const followUps = config?.suggestions?.followUps;
211
+ const legacy = config?.features?.suggestReplies;
212
+ const warnIfConflicting = (key: "enabled" | "expose"): void => {
213
+ const newValue = followUps?.[key];
214
+ const legacyValue = legacy?.[key];
215
+ if (newValue === undefined || legacyValue === undefined) return;
216
+ if (newValue === legacyValue) return;
217
+ warnConflict(key, legacyValue, newValue);
218
+ };
219
+ if (config) {
220
+ warnIfConflicting("enabled");
221
+ warnIfConflicting("expose");
222
+ }
223
+ const enabled = followUps?.enabled ?? legacy?.enabled ?? true;
224
+ const expose = enabled
225
+ ? followUps?.expose ?? legacy?.expose ?? false
226
+ : false;
227
+ return { enabled, expose };
152
228
  };
@@ -9,6 +9,40 @@ import {
9
9
  } from "./preview-utils";
10
10
 
11
11
  describe("theme editor preview demo data", () => {
12
+ it("keeps the home scene in a true pre-conversation state", () => {
13
+ const config = buildPreviewConfig({ scene: "home" });
14
+
15
+ expect(config.initialMessages).toEqual([]);
16
+ expect(config.suggestionChips?.length).toBeGreaterThan(0);
17
+ });
18
+
19
+ it("seeds rich agent suggestions in the follow-ups scene", () => {
20
+ const config = buildPreviewConfig({ scene: "follow-ups" });
21
+ const messages = config.initialMessages ?? [];
22
+ const suggestionMessage = messages.find(
23
+ (message) =>
24
+ message.variant === "tool" &&
25
+ message.toolCall?.name === "suggest_replies",
26
+ );
27
+ const suggestions = (
28
+ suggestionMessage?.toolCall?.args as
29
+ | { suggestions?: Array<Record<string, unknown>> }
30
+ | undefined
31
+ )?.suggestions;
32
+
33
+ expect(messages.some((message) => message.role === "user")).toBe(true);
34
+ expect(suggestionMessage?.toolCall?.status).toBe("complete");
35
+ expect(suggestions).toHaveLength(4);
36
+ expect(suggestions?.[0]).toEqual(
37
+ expect.objectContaining({
38
+ label: "Show me an implementation",
39
+ description: expect.any(String),
40
+ icon: "code-2",
41
+ emphasis: "primary",
42
+ }),
43
+ );
44
+ });
45
+
12
46
  it("seeds tool call preview messages when advanced tool display modes are enabled", () => {
13
47
  const config = buildPreviewConfig({
14
48
  scene: "conversation",
@@ -195,7 +195,12 @@ export function buildSrcdoc(
195
195
 
196
196
  // ─── Preview Messages ───────────────────────────────────────────
197
197
 
198
- export type PreviewScene = 'home' | 'conversation' | 'minimized' | 'artifact';
198
+ export type PreviewScene =
199
+ | 'home'
200
+ | 'conversation'
201
+ | 'follow-ups'
202
+ | 'minimized'
203
+ | 'artifact';
199
204
 
200
205
  export type PreviewTranscriptEntryPreset =
201
206
  | 'user-message'
@@ -543,8 +548,78 @@ export function createPreviewMessages(
543
548
  config?: Partial<AgentWidgetConfig>,
544
549
  appendedMessages: AgentWidgetMessage[] = []
545
550
  ): AgentWidgetMessage[] {
551
+ if (scene === 'follow-ups') {
552
+ const now = Date.now();
553
+ return [
554
+ {
555
+ id: 'preview-follow-up-user',
556
+ role: 'user',
557
+ content: 'What can Persona help me build?',
558
+ createdAt: new Date(now - 120000).toISOString(),
559
+ },
560
+ {
561
+ id: 'preview-follow-up-assistant',
562
+ role: 'assistant',
563
+ content:
564
+ 'Persona gives you a themeable streaming chat interface with flexible integrations and interaction patterns. Where would you like to go next?',
565
+ createdAt: new Date(now - 60000).toISOString(),
566
+ },
567
+ {
568
+ id: 'preview-follow-up-suggestions',
569
+ role: 'assistant',
570
+ content: '',
571
+ createdAt: new Date(now - 59000).toISOString(),
572
+ streaming: false,
573
+ variant: 'tool',
574
+ toolCall: {
575
+ id: 'preview-follow-up-suggestions',
576
+ name: 'suggest_replies',
577
+ status: 'complete',
578
+ args: {
579
+ suggestions: [
580
+ {
581
+ id: 'preview-implementation',
582
+ label: 'Show me an implementation',
583
+ prompt: 'Show me a practical implementation',
584
+ description: 'See a complete integration example',
585
+ icon: 'code-2',
586
+ emphasis: 'primary',
587
+ },
588
+ {
589
+ id: 'preview-theming',
590
+ label: 'Explore theming',
591
+ prompt: 'Tell me how theming works',
592
+ description: 'Customize colors, type, and component styles',
593
+ icon: 'palette',
594
+ },
595
+ {
596
+ id: 'preview-security',
597
+ label: 'Review security',
598
+ prompt: 'Explain the security model',
599
+ description: 'Learn about isolation and safe rendering',
600
+ icon: 'shield-check',
601
+ },
602
+ {
603
+ id: 'preview-support',
604
+ label: 'Talk to support',
605
+ prompt: 'Help me contact support',
606
+ description: 'Get help choosing the right approach',
607
+ icon: 'messages-square',
608
+ },
609
+ ],
610
+ },
611
+ chunks: [],
612
+ },
613
+ },
614
+ ...appendedMessages,
615
+ ];
616
+ }
546
617
  if (scene === 'home') {
547
- return [{ id: 'preview-home-1', role: 'assistant', content: 'Hi there! How can we help today?', createdAt: new Date().toISOString() }];
618
+ // Home is the pre-conversation state: welcome copy + starter suggestions
619
+ // already provide the greeting and entry points. Seeding an assistant
620
+ // bubble here creates a duplicate greeting that the default runtime never
621
+ // enters unless a host explicitly configures its own initialMessages.
622
+ return [];
548
623
  }
549
624
  if (scene === 'minimized') {
550
625
  return [{ id: 'preview-min-1', role: 'assistant', content: 'We are here whenever you are ready.', createdAt: new Date().toISOString() }];
@@ -60,6 +60,39 @@ describe("theme editor scroll-to-bottom controls", () => {
60
60
  expect(fieldPaths).toContain("theme.components.scrollToBottom.iconSize");
61
61
  });
62
62
 
63
+ it("exposes suggestion behavior and component token controls", () => {
64
+ const suggestions = CONFIGURE_SECTIONS.find(
65
+ (section) => section.id === "suggestions"
66
+ );
67
+ const configPaths =
68
+ suggestions?.fields.map((field) => field.path) ?? [];
69
+ expect(configPaths).toEqual(
70
+ expect.arrayContaining([
71
+ "suggestions.starters.variant",
72
+ "suggestions.starters.placement",
73
+ "suggestions.starters.behavior",
74
+ "suggestions.followUps.variant",
75
+ "suggestions.followUps.placement",
76
+ "suggestions.followUps.behavior",
77
+ "suggestions.followUps.overflow",
78
+ ])
79
+ );
80
+
81
+ const componentPaths = COMPONENTS_SECTIONS.flatMap((section) =>
82
+ section.fields.map((field) => field.path)
83
+ );
84
+ expect(componentPaths).toEqual(
85
+ expect.arrayContaining([
86
+ "theme.components.suggestion.chip.background",
87
+ "theme.components.suggestion.chip.hoverBackground",
88
+ "theme.components.suggestion.chip.focusRing",
89
+ "theme.components.suggestion.card.background",
90
+ "theme.components.suggestion.card.shadow",
91
+ "theme.components.suggestion.list.background",
92
+ ])
93
+ );
94
+ });
95
+
63
96
  it("exposes a shadow control for every themeable component", () => {
64
97
  const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
65
98
 
@@ -369,6 +369,18 @@ const inputShapeSectionDef: SectionDef = {
369
369
  ],
370
370
  };
371
371
 
372
+ const composerSpacingSectionDef: SectionDef = {
373
+ id: 'comp-composer-spacing',
374
+ title: 'Composer Spacing',
375
+ collapsed: true,
376
+ fields: [
377
+ { id: 'composer-padding', label: 'Padding', type: 'text', path: 'theme.components.composer.padding', defaultValue: '0.75rem 1rem' },
378
+ { id: 'composer-gap', label: 'Row Gap', description: 'Space between the textarea row and the actions row', type: 'text', path: 'theme.components.composer.gap', defaultValue: '0.5rem' },
379
+ { id: 'composer-font-size', label: 'Font Size', type: 'text', path: 'theme.components.composer.fontSize', defaultValue: '0.875rem' },
380
+ { id: 'composer-line-height', label: 'Line Height', type: 'text', path: 'theme.components.composer.lineHeight', defaultValue: '1.25rem' },
381
+ ],
382
+ };
383
+
372
384
  const buttonShapeSectionDef: SectionDef = {
373
385
  id: 'comp-button-shape',
374
386
  title: 'Button Shape',
@@ -466,6 +478,32 @@ const scrollToBottomSectionDef: SectionDef = {
466
478
  ],
467
479
  };
468
480
 
481
+ const suggestionStyleSectionDef: SectionDef = {
482
+ id: 'comp-suggestions',
483
+ title: 'Suggestions',
484
+ description: 'Style starter cards, follow-up chips, and list rows.',
485
+ collapsed: true,
486
+ fields: [
487
+ { id: 'suggestion-chip-bg', label: 'Chip Background', type: 'token-ref', path: 'theme.components.suggestion.chip.background', defaultValue: 'semantic.colors.surface', tokenRef: { tokenType: 'color' } },
488
+ { id: 'suggestion-chip-fg', label: 'Chip Text', type: 'token-ref', path: 'theme.components.suggestion.chip.foreground', defaultValue: 'semantic.colors.text', tokenRef: { tokenType: 'color' } },
489
+ { id: 'suggestion-chip-border', label: 'Chip Border', type: 'token-ref', path: 'theme.components.suggestion.chip.border', defaultValue: 'semantic.colors.border', tokenRef: { tokenType: 'color' } },
490
+ { id: 'suggestion-chip-hover', label: 'Chip Hover', type: 'token-ref', path: 'theme.components.suggestion.chip.hoverBackground', defaultValue: 'palette.colors.gray.100', tokenRef: { tokenType: 'color' } },
491
+ { id: 'suggestion-chip-focus', label: 'Chip Focus Ring', type: 'token-ref', path: 'theme.components.suggestion.chip.focusRing', defaultValue: 'semantic.colors.interactive.focus', tokenRef: { tokenType: 'color' } },
492
+ { id: 'suggestion-card-bg', label: 'Card Background', type: 'token-ref', path: 'theme.components.suggestion.card.background', defaultValue: 'semantic.colors.surface', tokenRef: { tokenType: 'color' } },
493
+ { id: 'suggestion-card-fg', label: 'Card Text', type: 'token-ref', path: 'theme.components.suggestion.card.foreground', defaultValue: 'semantic.colors.text', tokenRef: { tokenType: 'color' } },
494
+ { id: 'suggestion-card-border', label: 'Card Border', type: 'token-ref', path: 'theme.components.suggestion.card.border', defaultValue: 'semantic.colors.border', tokenRef: { tokenType: 'color' } },
495
+ { id: 'suggestion-card-hover', label: 'Card Hover', type: 'token-ref', path: 'theme.components.suggestion.card.hoverBackground', defaultValue: 'palette.colors.gray.100', tokenRef: { tokenType: 'color' } },
496
+ { id: 'suggestion-card-shadow', label: 'Card Shadow', type: 'select', path: 'theme.components.suggestion.card.shadow', defaultValue: 'palette.shadows.sm', options: [
497
+ { value: 'palette.shadows.none', label: 'None' },
498
+ { value: 'palette.shadows.sm', label: 'Small' },
499
+ { value: 'palette.shadows.md', label: 'Medium' },
500
+ { value: 'palette.shadows.lg', label: 'Large' },
501
+ ] },
502
+ { id: 'suggestion-list-bg', label: 'List Background', type: 'color', path: 'theme.components.suggestion.list.background', defaultValue: 'transparent' },
503
+ { id: 'suggestion-list-border', label: 'List Border', type: 'token-ref', path: 'theme.components.suggestion.list.border', defaultValue: 'semantic.colors.border', tokenRef: { tokenType: 'color' } },
504
+ ],
505
+ };
506
+
469
507
  // Reusable shadow-scale options matching the launcher / panel / scroll-to-bottom selects.
470
508
  const SHADOW_SCALE_OPTIONS: { value: string; label: string }[] = [
471
509
  { value: 'palette.shadows.none', label: 'None' },
@@ -502,6 +540,7 @@ export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
502
540
  launcherLayoutSectionDef,
503
541
  messageShapeSectionDef,
504
542
  inputShapeSectionDef,
543
+ composerSpacingSectionDef,
505
544
  buttonShapeSectionDef,
506
545
  componentShadowsSectionDef,
507
546
  ];
@@ -513,6 +552,7 @@ export const COMPONENT_COLOR_SECTIONS: SectionDef[] = [
513
552
  inputColorsSectionDef,
514
553
  buttonColorsSectionDef,
515
554
  scrollToBottomSectionDef,
555
+ suggestionStyleSectionDef,
516
556
  ];
517
557
 
518
558
  export const COMPONENTS_SECTIONS: SectionDef[] = [
@@ -526,9 +566,9 @@ export const COMPONENTS_SECTIONS: SectionDef[] = [
526
566
 
527
567
  const MB = 1024 * 1024;
528
568
  const DEFAULT_SUGGESTION_CHIPS = [
529
- 'What can you help me with?',
530
- 'Tell me about your features',
531
- 'How does this work?',
569
+ 'Show me what you can help with',
570
+ 'Walk me through getting started',
571
+ 'Answer a question about this page',
532
572
  ];
533
573
 
534
574
  const ATTACHMENT_TYPE_PRESETS: Record<string, string[]> = {
@@ -576,9 +616,17 @@ const copySectionDef: SectionDef = {
576
616
  };
577
617
 
578
618
  const suggestionsSectionDef: SectionDef = {
579
- id: 'suggestions', title: 'Suggestion Chips', description: 'Configure chip content and styling.', collapsed: true,
619
+ id: 'suggestions', title: 'Suggestions', description: 'Configure starter prompts and agent follow-ups.', collapsed: true,
580
620
  fields: [
581
- { id: 'suggestions-list', label: 'Suggestions', description: 'Add, edit, and remove chips directly.', type: 'chip-list', path: 'suggestionChips', defaultValue: DEFAULT_SUGGESTION_CHIPS },
621
+ { id: 'suggestions-list', label: 'Starter Prompts', description: 'Add, edit, and remove starter prompts directly.', type: 'chip-list', path: 'suggestionChips', defaultValue: DEFAULT_SUGGESTION_CHIPS },
622
+ { id: 'suggestions-starter-variant', label: 'Starter Style', type: 'select', path: 'suggestions.starters.variant', defaultValue: 'card', options: [{ value: 'card', label: 'Cards' }, { value: 'chip', label: 'Chips' }, { value: 'list', label: 'List' }] },
623
+ { id: 'suggestions-starter-placement', label: 'Starter Placement', type: 'select', path: 'suggestions.starters.placement', defaultValue: 'auto', options: [{ value: 'auto', label: 'Automatic' }, { value: 'welcome', label: 'Welcome Surface' }, { value: 'composer', label: 'Above Composer' }] },
624
+ { id: 'suggestions-starter-behavior', label: 'Starter Click', type: 'select', path: 'suggestions.starters.behavior', defaultValue: 'send', options: [{ value: 'send', label: 'Send Immediately' }, { value: 'fill', label: 'Fill Composer' }] },
625
+ { id: 'suggestions-starter-overflow', label: 'Starter Overflow', type: 'select', path: 'suggestions.starters.overflow', defaultValue: 'wrap', options: [{ value: 'wrap', label: 'Wrap Rows' }, { value: 'scroll', label: 'Horizontal Scroll' }] },
626
+ { id: 'suggestions-followup-variant', label: 'Follow-up Style', type: 'select', path: 'suggestions.followUps.variant', defaultValue: 'chip', options: [{ value: 'chip', label: 'Chips' }, { value: 'card', label: 'Cards' }, { value: 'list', label: 'List' }] },
627
+ { id: 'suggestions-followup-placement', label: 'Follow-up Placement', type: 'select', path: 'suggestions.followUps.placement', defaultValue: 'auto', options: [{ value: 'auto', label: 'Automatic' }, { value: 'after-message', label: 'After Messages' }, { value: 'composer', label: 'Above Composer' }] },
628
+ { id: 'suggestions-followup-behavior', label: 'Follow-up Click', type: 'select', path: 'suggestions.followUps.behavior', defaultValue: 'send', options: [{ value: 'send', label: 'Send Immediately' }, { value: 'fill', label: 'Fill Composer' }] },
629
+ { id: 'suggestions-followup-overflow', label: 'Follow-up Overflow', type: 'select', path: 'suggestions.followUps.overflow', defaultValue: 'scroll', options: [{ value: 'scroll', label: 'Horizontal Scroll' }, { value: 'wrap', label: 'Wrap Rows' }] },
582
630
  ],
583
631
  };
584
632
 
@@ -142,7 +142,8 @@ export const THEME_TOKEN_DOCS = {
142
142
  'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
143
143
  toolBubble: 'shadow: tool call row box-shadow.',
144
144
  reasoningBubble: 'shadow: reasoning/thinking row box-shadow.',
145
- composer: 'shadow: message input form box-shadow.',
145
+ composer:
146
+ 'Message input form: shadow (box-shadow), padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type).',
146
147
  artifact:
147
148
  'toolbar (icon styling, copy menu), tab (background, active states), pane (background, toolbarBackground).',
148
149
  },
@@ -245,8 +246,14 @@ export const THEME_TOKEN_DOCS = {
245
246
  properties:
246
247
  'enabled, provider ("browser" | "runtype"), browserFallback, voice, rate, pitch.',
247
248
  },
249
+ suggestions: {
250
+ description:
251
+ 'Rich starter and follow-up suggestion surfaces. Strings remain valid item shorthand.',
252
+ properties:
253
+ 'starters.items, starters.variant ("card" | "chip" | "list"), starters.placement ("auto" | "welcome" | "composer"), starters.behavior ("send" | "fill"), starters.overflow ("scroll" | "wrap"), starters.maxItems, followUps.enabled, followUps.expose, followUps.variant, followUps.placement ("auto" | "after-message" | "composer"), followUps.behavior, followUps.overflow ("scroll" | "wrap"), followUps.maxItems.',
254
+ },
248
255
  suggestionChips:
249
- 'string[]: Suggested prompts shown to the user.',
256
+ 'Deprecated string[] shorthand for starter prompts. Prefer suggestions.starters.items.',
250
257
  messageActions: {
251
258
  description: 'Message action buttons (copy, upvote, downvote).',
252
259
  properties:
@@ -365,6 +365,14 @@ export interface ReasoningBubbleTokens {
365
365
  export interface ComposerChromeTokens {
366
366
  /** Box-shadow on the composer form (raw CSS, e.g. `none`). */
367
367
  shadow: string;
368
+ /** Inner padding of the composer form (raw CSS shorthand). @default "0.75rem 1rem" */
369
+ padding?: string;
370
+ /** Gap between the textarea row and the actions row. @default "0.5rem" */
371
+ gap?: string;
372
+ /** Composer textarea font-size. @default "0.875rem" */
373
+ fontSize?: string;
374
+ /** Composer textarea line-height. @default "1.25rem" */
375
+ lineHeight?: string;
368
376
  }
369
377
 
370
378
  /** Artifact toolbar chrome. */
@@ -524,6 +532,28 @@ export interface ScrollToBottomTokens extends ComponentTokenSet {
524
532
  iconSize?: string;
525
533
  }
526
534
 
535
+ /** Visual tokens shared by one suggestion presentation variant. */
536
+ export interface SuggestionVariantTokens extends ComponentTokenSet {
537
+ gap?: string;
538
+ minHeight?: string;
539
+ fontSize?: string;
540
+ lineHeight?: string;
541
+ iconSize?: string;
542
+ hoverBackground?: TokenReference<'color'>;
543
+ hoverForeground?: TokenReference<'color'>;
544
+ hoverBorder?: TokenReference<'color'>;
545
+ pressedBackground?: TokenReference<'color'>;
546
+ focusRing?: TokenReference<'color'>;
547
+ disabledOpacity?: string;
548
+ }
549
+
550
+ /** Starter and follow-up suggestion chrome. */
551
+ export interface SuggestionTokens {
552
+ chip: SuggestionVariantTokens;
553
+ card: SuggestionVariantTokens;
554
+ list: SuggestionVariantTokens;
555
+ }
556
+
527
557
  /** Toggle group chrome (used by createToggleGroup). */
528
558
  export interface ToggleGroupTokens {
529
559
  /** Gap between toggle buttons. Default: 0 (connected). */
@@ -555,6 +585,8 @@ export interface ComponentTokens {
555
585
  labelButton?: LabelButtonTokens;
556
586
  /** Scroll-to-bottom indicator styling tokens. */
557
587
  scrollToBottom?: ScrollToBottomTokens;
588
+ /** Starter prompt and follow-up suggestion styling tokens. */
589
+ suggestion?: SuggestionTokens;
558
590
  /** Toggle group styling tokens. */
559
591
  toggleGroup?: ToggleGroupTokens;
560
592
  /** Artifact toolbar, tab strip, and pane chrome. */