@runtypelabs/persona 3.16.0 → 3.18.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 (71) hide show
  1. package/README.md +142 -0
  2. package/dist/animations/glyph-cycle.cjs +279 -0
  3. package/dist/animations/glyph-cycle.d.cts +5 -0
  4. package/dist/animations/glyph-cycle.d.ts +5 -0
  5. package/dist/animations/glyph-cycle.js +252 -0
  6. package/dist/animations/types-cwY5HaFD.d.cts +307 -0
  7. package/dist/animations/types-cwY5HaFD.d.ts +307 -0
  8. package/dist/animations/wipe.cjs +107 -0
  9. package/dist/animations/wipe.d.cts +5 -0
  10. package/dist/animations/wipe.d.ts +5 -0
  11. package/dist/animations/wipe.js +80 -0
  12. package/dist/index.cjs +49 -48
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +504 -1
  15. package/dist/index.d.ts +504 -1
  16. package/dist/index.global.js +143 -88
  17. package/dist/index.global.js.map +1 -1
  18. package/dist/index.js +49 -48
  19. package/dist/index.js.map +1 -1
  20. package/dist/testing.cjs +85 -0
  21. package/dist/testing.d.cts +39 -0
  22. package/dist/testing.d.ts +39 -0
  23. package/dist/testing.js +56 -0
  24. package/dist/theme-editor.cjs +2095 -207
  25. package/dist/theme-editor.d.cts +432 -2
  26. package/dist/theme-editor.d.ts +432 -2
  27. package/dist/theme-editor.js +2093 -207
  28. package/dist/theme-reference.cjs +1 -1
  29. package/dist/theme-reference.d.cts +14 -0
  30. package/dist/theme-reference.d.ts +14 -0
  31. package/dist/widget.css +565 -0
  32. package/package.json +20 -3
  33. package/src/animations/glyph-cycle.ts +332 -0
  34. package/src/animations/wipe.ts +66 -0
  35. package/src/client.test.ts +275 -0
  36. package/src/client.ts +99 -0
  37. package/src/components/ask-user-question-bubble.test.ts +583 -0
  38. package/src/components/ask-user-question-bubble.ts +924 -0
  39. package/src/components/composer-builder.ts +61 -10
  40. package/src/components/message-bubble.test.ts +181 -2
  41. package/src/components/message-bubble.ts +209 -14
  42. package/src/components/messages.ts +33 -1
  43. package/src/components/panel.ts +45 -5
  44. package/src/defaults.ts +37 -0
  45. package/src/index-global.ts +31 -0
  46. package/src/index.ts +34 -1
  47. package/src/plugins/types.ts +57 -0
  48. package/src/session.test.ts +276 -1
  49. package/src/session.ts +247 -3
  50. package/src/styles/widget.css +565 -0
  51. package/src/testing/index.ts +11 -0
  52. package/src/testing/mock-stream.test.ts +80 -0
  53. package/src/testing/mock-stream.ts +94 -0
  54. package/src/testing.ts +2 -0
  55. package/src/theme-editor/index.ts +4 -0
  56. package/src/theme-editor/preview-utils.test.ts +60 -0
  57. package/src/theme-editor/preview-utils.ts +129 -0
  58. package/src/theme-editor/sections.test.ts +19 -0
  59. package/src/theme-editor/sections.ts +84 -1
  60. package/src/types/theme.ts +15 -0
  61. package/src/types.ts +360 -0
  62. package/src/ui.ask-user-question-plugin.test.ts +649 -0
  63. package/src/ui.stop-button.test.ts +165 -0
  64. package/src/ui.ts +706 -11
  65. package/src/utils/message-fingerprint.ts +2 -0
  66. package/src/utils/morph.ts +7 -0
  67. package/src/utils/storage.ts +10 -2
  68. package/src/utils/stream-animation.test.ts +417 -0
  69. package/src/utils/stream-animation.ts +449 -0
  70. package/src/utils/theme.test.ts +36 -0
  71. package/src/utils/tokens.ts +23 -0
@@ -0,0 +1,94 @@
1
+ /** Shared helpers for mocking SSE streams in demos, previews, and tests. */
2
+
3
+ export interface MockSSEFrame {
4
+ type: string;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ export interface CreateMockSSEStreamOptions {
9
+ /** Delay in ms between emitted frames. Default: 100. */
10
+ delayMs?: number;
11
+ /**
12
+ * Named event name. When set, each frame is emitted as `event: <name>\ndata: ...\n\n`.
13
+ * Omit for bare `data: ...\n\n` form (both are valid SSE and the widget parser accepts either).
14
+ */
15
+ eventName?: string;
16
+ }
17
+
18
+ const encoder = new TextEncoder();
19
+
20
+ export function createMockSSEStream(
21
+ frames: ReadonlyArray<MockSSEFrame>,
22
+ options?: CreateMockSSEStreamOptions
23
+ ): ReadableStream<Uint8Array> {
24
+ const delayMs = options?.delayMs ?? 100;
25
+ const prefix = options?.eventName ? `event: ${options.eventName}\n` : "";
26
+ let index = 0;
27
+
28
+ return new ReadableStream<Uint8Array>({
29
+ async pull(controller) {
30
+ if (index >= frames.length) {
31
+ controller.close();
32
+ return;
33
+ }
34
+ await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
35
+ const payload = JSON.stringify(frames[index]);
36
+ controller.enqueue(encoder.encode(`${prefix}data: ${payload}\n\n`));
37
+ index += 1;
38
+ },
39
+ });
40
+ }
41
+
42
+ export interface AssistantTurnFramesOptions {
43
+ /** Execution id shared across the turn's frames. */
44
+ executionId: string;
45
+ /** Turn id. Default: `turn-1`. */
46
+ turnId?: string;
47
+ /** Assistant text content to stream. */
48
+ text: string;
49
+ /** Approximate characters per `agent_turn_delta` frame. Default: 32. */
50
+ chunkSize?: number;
51
+ }
52
+
53
+ /**
54
+ * Builds the standard `agent_turn_start` → many `agent_turn_delta` → `agent_turn_complete`
55
+ * frame sequence for simulating a streaming assistant reply. The frames drive the same
56
+ * client pipeline as real SSE, so stream animations (typewriter, word-fade, etc.) engage.
57
+ */
58
+ export function buildAssistantTurnFrames(options: AssistantTurnFramesOptions): MockSSEFrame[] {
59
+ const { executionId, text } = options;
60
+ const turnId = options.turnId ?? "turn-1";
61
+ const chunkSize = Math.max(1, options.chunkSize ?? 32);
62
+
63
+ const frames: MockSSEFrame[] = [{ type: "agent_turn_start", executionId, turnId }];
64
+ for (let i = 0; i < text.length; i += chunkSize) {
65
+ frames.push({
66
+ type: "agent_turn_delta",
67
+ executionId,
68
+ turnId,
69
+ delta: text.slice(i, i + chunkSize),
70
+ });
71
+ }
72
+ frames.push({ type: "agent_turn_complete", executionId, turnId });
73
+ return frames;
74
+ }
75
+
76
+ export interface MockSSEResponseOptions extends CreateMockSSEStreamOptions {
77
+ status?: number;
78
+ headers?: Record<string, string>;
79
+ }
80
+
81
+ /** Convenience wrapper: returns a `Response` ready to hand back from a `customFetch` implementation. */
82
+ export function createMockSSEResponse(
83
+ frames: ReadonlyArray<MockSSEFrame>,
84
+ options?: MockSSEResponseOptions
85
+ ): Response {
86
+ const stream = createMockSSEStream(frames, options);
87
+ return new Response(stream, {
88
+ status: options?.status ?? 200,
89
+ headers: {
90
+ "Content-Type": "text/event-stream",
91
+ ...options?.headers,
92
+ },
93
+ });
94
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Entry point for @runtypelabs/persona/testing */
2
+ export * from "./testing/index";
@@ -84,6 +84,8 @@ export {
84
84
  getPreviewTranscriptPresetLabel,
85
85
  createPreviewTranscriptEntry,
86
86
  appendPreviewTranscriptEntry,
87
+ presetStreamsText,
88
+ buildTranscriptStreamFrames,
87
89
  createPreviewMessages,
88
90
  applySceneConfig,
89
91
  buildPreviewConfig,
@@ -94,6 +96,8 @@ export type {
94
96
  PreviewTranscriptEntryPreset,
95
97
  PreviewShellPalette,
96
98
  PreviewConfigOptions,
99
+ TranscriptStreamFrame,
100
+ BuildTranscriptStreamFramesOptions,
97
101
  } from './preview-utils';
98
102
 
99
103
  // Role mappings (Interface Roles editor)
@@ -3,7 +3,9 @@ import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  appendPreviewTranscriptEntry,
5
5
  buildPreviewConfig,
6
+ buildTranscriptStreamFrames,
6
7
  createPreviewTranscriptEntry,
8
+ presetStreamsText,
7
9
  } from "./preview-utils";
8
10
 
9
11
  describe("theme editor preview demo data", () => {
@@ -55,4 +57,62 @@ describe("theme editor preview demo data", () => {
55
57
  expect(updated[0].variant).toBe("tool");
56
58
  expect(updated[1].variant).toBe("reasoning");
57
59
  });
60
+
61
+ it("creates assistant code-block, table, and image presets with markdown content", () => {
62
+ const code = createPreviewTranscriptEntry("assistant-code-block", 1);
63
+ const table = createPreviewTranscriptEntry("assistant-markdown-table", 2);
64
+ const image = createPreviewTranscriptEntry("assistant-image", 3);
65
+
66
+ for (const message of [code, table, image]) {
67
+ expect(message.role).toBe("assistant");
68
+ expect(message.variant).toBeUndefined();
69
+ expect(typeof message.content).toBe("string");
70
+ }
71
+
72
+ expect(code.content).toContain("```ts");
73
+ expect(table.content).toContain("| Preset");
74
+ expect(image.content).toContain("![");
75
+ });
76
+
77
+ it("flags assistant text presets as streaming and non-text presets as not", () => {
78
+ expect(presetStreamsText("assistant-message")).toBe(true);
79
+ expect(presetStreamsText("assistant-code-block")).toBe(true);
80
+ expect(presetStreamsText("assistant-markdown-table")).toBe(true);
81
+ expect(presetStreamsText("assistant-image")).toBe(true);
82
+ expect(presetStreamsText("user-message")).toBe(false);
83
+ expect(presetStreamsText("reasoning-streaming")).toBe(false);
84
+ expect(presetStreamsText("tool-running")).toBe(false);
85
+ });
86
+
87
+ it("builds a single done frame for non-streaming presets", () => {
88
+ const frames = buildTranscriptStreamFrames("user-message", 0);
89
+ expect(frames).toHaveLength(1);
90
+ expect(frames[0].done).toBe(true);
91
+ expect(frames[0].delayMs).toBe(0);
92
+ expect(frames[0].message.role).toBe("user");
93
+ });
94
+
95
+ it("builds progressive snapshots for assistant text presets ending with streaming:false", () => {
96
+ const frames = buildTranscriptStreamFrames("assistant-message", 0, { chunkSize: 10, delayMs: 5 });
97
+ expect(frames.length).toBeGreaterThan(1);
98
+
99
+ const first = frames[0];
100
+ expect(first.message.content).toBe("");
101
+ expect(first.message.streaming).toBe(true);
102
+ expect(first.delayMs).toBe(0);
103
+ expect(first.done).toBe(false);
104
+
105
+ for (let i = 1; i < frames.length - 1; i += 1) {
106
+ expect(frames[i].message.streaming).toBe(true);
107
+ expect(frames[i].done).toBe(false);
108
+ expect(frames[i].delayMs).toBe(5);
109
+ }
110
+
111
+ const last = frames[frames.length - 1];
112
+ expect(last.message.streaming).toBe(false);
113
+ expect(last.done).toBe(true);
114
+ const completed = createPreviewTranscriptEntry("assistant-message", 0);
115
+ expect(last.message.content).toBe(completed.content);
116
+ expect(last.message.id).toBe(completed.id);
117
+ });
58
118
  });
@@ -200,6 +200,9 @@ export type PreviewScene = 'home' | 'conversation' | 'minimized' | 'artifact';
200
200
  export type PreviewTranscriptEntryPreset =
201
201
  | 'user-message'
202
202
  | 'assistant-message'
203
+ | 'assistant-code-block'
204
+ | 'assistant-markdown-table'
205
+ | 'assistant-image'
203
206
  | 'reasoning-streaming'
204
207
  | 'reasoning-complete'
205
208
  | 'tool-running'
@@ -208,6 +211,9 @@ export type PreviewTranscriptEntryPreset =
208
211
  const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, string> = {
209
212
  'user-message': 'User message',
210
213
  'assistant-message': 'Assistant message',
214
+ 'assistant-code-block': 'Assistant — code block',
215
+ 'assistant-markdown-table': 'Assistant — markdown table',
216
+ 'assistant-image': 'Assistant — image',
211
217
  'reasoning-streaming': 'Reasoning (streaming)',
212
218
  'reasoning-complete': 'Reasoning (complete)',
213
219
  'tool-running': 'Tool call (running)',
@@ -240,6 +246,56 @@ export function createPreviewTranscriptEntry(
240
246
  content: 'Absolutely. I can keep going and explain what happens next.',
241
247
  createdAt,
242
248
  };
249
+ case 'assistant-code-block':
250
+ return {
251
+ id: `preview-seq-assistant-code-${suffix}`,
252
+ role: 'assistant',
253
+ content: [
254
+ "Here's how you'd wire up a streaming animation:",
255
+ '',
256
+ '```ts',
257
+ "import { createAgentExperience } from '@runtypelabs/persona';",
258
+ '',
259
+ 'createAgentExperience(el, {',
260
+ ' features: {',
261
+ ' streamAnimation: { type: "letter-rise", speed: 120 },',
262
+ ' },',
263
+ '});',
264
+ '```',
265
+ '',
266
+ 'Swap the `type` value to try the other presets.',
267
+ ].join('\n'),
268
+ createdAt,
269
+ };
270
+ case 'assistant-markdown-table':
271
+ return {
272
+ id: `preview-seq-assistant-table-${suffix}`,
273
+ role: 'assistant',
274
+ content: [
275
+ 'Here are the built-in streaming animations at a glance:',
276
+ '',
277
+ '| Preset | Wrap unit | Best for |',
278
+ '| ------------ | --------- | --------------------------- |',
279
+ '| Typewriter | Character | Classic terminal feel |',
280
+ '| Letter rise | Character | Soft, staggered entrance |',
281
+ '| Word fade | Word | Longer-form assistant replies |',
282
+ '| Pop bubble | Bubble | Short, punchy affirmations |',
283
+ ].join('\n'),
284
+ createdAt,
285
+ };
286
+ case 'assistant-image':
287
+ return {
288
+ id: `preview-seq-assistant-image-${suffix}`,
289
+ role: 'assistant',
290
+ content: [
291
+ "Here's the reference diagram you asked for — let me know if you'd like a different view:",
292
+ '',
293
+ '![Stream animation reference](https://placehold.co/320x200/png?text=Stream+Animation)',
294
+ '',
295
+ 'The gradient shows how per-unit delays stagger across the reply.',
296
+ ].join('\n'),
297
+ createdAt,
298
+ };
243
299
  case 'reasoning-streaming':
244
300
  return {
245
301
  id: `preview-seq-reasoning-stream-${suffix}`,
@@ -312,6 +368,79 @@ export function appendPreviewTranscriptEntry(
312
368
  return [...messages, createPreviewTranscriptEntry(preset, messages.length)];
313
369
  }
314
370
 
371
+ /** Presets whose assistant content should stream in so Stream Animation settings engage. */
372
+ export function presetStreamsText(preset: PreviewTranscriptEntryPreset): boolean {
373
+ return (
374
+ preset === 'assistant-message' ||
375
+ preset === 'assistant-code-block' ||
376
+ preset === 'assistant-markdown-table' ||
377
+ preset === 'assistant-image'
378
+ );
379
+ }
380
+
381
+ export interface TranscriptStreamFrame {
382
+ /** Message to upsert into the session. */
383
+ message: AgentWidgetMessage;
384
+ /** Delay from the previous frame in ms. The first frame uses 0. */
385
+ delayMs: number;
386
+ /** True when this is the final frame (message is no longer streaming). */
387
+ done: boolean;
388
+ }
389
+
390
+ export interface BuildTranscriptStreamFramesOptions {
391
+ /** Characters per progressive chunk. Default: 24. */
392
+ chunkSize?: number;
393
+ /** Delay between chunks in ms. Default: 42. */
394
+ delayMs?: number;
395
+ }
396
+
397
+ /**
398
+ * Builds progressive snapshots for a transcript preset suitable for feeding into
399
+ * `injectTestMessage({ type: 'message', message })` on a timer. Each frame upserts
400
+ * the same message id with more content, ending with `streaming: false`.
401
+ *
402
+ * - Streaming-capable presets (assistant text) yield many frames.
403
+ * - All other presets yield a single `done` frame matching `createPreviewTranscriptEntry`.
404
+ */
405
+ export function buildTranscriptStreamFrames(
406
+ preset: PreviewTranscriptEntryPreset,
407
+ suffix: number,
408
+ options?: BuildTranscriptStreamFramesOptions
409
+ ): TranscriptStreamFrame[] {
410
+ const completed = createPreviewTranscriptEntry(preset, suffix);
411
+ if (!presetStreamsText(preset) || typeof completed.content !== 'string') {
412
+ return [{ message: completed, delayMs: 0, done: true }];
413
+ }
414
+
415
+ const chunkSize = Math.max(1, options?.chunkSize ?? 24);
416
+ const delayMs = Math.max(0, options?.delayMs ?? 42);
417
+ const fullText = completed.content;
418
+ const frames: TranscriptStreamFrame[] = [];
419
+
420
+ // Seed with an empty streaming bubble so the animation plugin can attach from the first tick.
421
+ frames.push({
422
+ message: { ...completed, content: '', streaming: true },
423
+ delayMs: 0,
424
+ done: false,
425
+ });
426
+
427
+ for (let i = chunkSize; i < fullText.length; i += chunkSize) {
428
+ frames.push({
429
+ message: { ...completed, content: fullText.slice(0, i), streaming: true },
430
+ delayMs,
431
+ done: false,
432
+ });
433
+ }
434
+
435
+ frames.push({
436
+ message: { ...completed, content: fullText, streaming: false },
437
+ delayMs,
438
+ done: true,
439
+ });
440
+
441
+ return frames;
442
+ }
443
+
315
444
  const createAdvancedTranscriptPreviewMessages = (): AgentWidgetMessage[] => [
316
445
  {
317
446
  id: "preview-adv-1",
@@ -60,4 +60,23 @@ describe("theme editor scroll-to-bottom controls", () => {
60
60
  expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.previewMaxLines")).toBe(true);
61
61
  expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activeMinHeight")).toBe(true);
62
62
  });
63
+
64
+ it("exposes stream animation controls", () => {
65
+ const section = CONFIGURE_SECTIONS.find((entry) => entry.id === "stream-animation");
66
+
67
+ expect(section).toBeDefined();
68
+ const paths = section?.fields.map((field) => field.path) ?? [];
69
+ expect(paths).toEqual(
70
+ expect.arrayContaining([
71
+ "features.streamAnimation.type",
72
+ "features.streamAnimation.placeholder",
73
+ "features.streamAnimation.buffer",
74
+ "features.streamAnimation.speed",
75
+ "features.streamAnimation.duration",
76
+ ])
77
+ );
78
+
79
+ const speedField = section?.fields.find((field) => field.path === "features.streamAnimation.speed");
80
+ expect(speedField?.parseValue?.("240")).toBe(240);
81
+ });
63
82
  });
@@ -697,6 +697,89 @@ const featuresSectionDef: SectionDef = {
697
697
  ],
698
698
  };
699
699
 
700
+ const streamAnimationSectionDef: SectionDef = {
701
+ id: 'stream-animation', title: 'Stream Animation', description: 'Control how assistant text appears while streaming.', collapsed: true,
702
+ fields: [
703
+ {
704
+ id: 'stream-anim-type',
705
+ label: 'Animation',
706
+ description: 'Reveal effect applied to each assistant reply as it streams.',
707
+ type: 'select',
708
+ path: 'features.streamAnimation.type',
709
+ defaultValue: 'none',
710
+ options: [
711
+ { value: 'none', label: 'None' },
712
+ { value: 'typewriter', label: 'Typewriter' },
713
+ { value: 'word-fade', label: 'Word fade' },
714
+ { value: 'letter-rise', label: 'Letter rise' },
715
+ { value: 'glyph-cycle', label: 'Glyph cycle' },
716
+ { value: 'wipe', label: 'Wipe' },
717
+ { value: 'pop-bubble', label: 'Pop bubble' },
718
+ ],
719
+ },
720
+ {
721
+ id: 'stream-anim-placeholder',
722
+ label: 'Pre-first-token Placeholder',
723
+ description: 'What to show before the first token arrives.',
724
+ type: 'select',
725
+ path: 'features.streamAnimation.placeholder',
726
+ defaultValue: 'none',
727
+ options: [
728
+ { value: 'none', label: 'Typing indicator (default)' },
729
+ { value: 'skeleton', label: 'Skeleton shimmer' },
730
+ ],
731
+ },
732
+ {
733
+ id: 'stream-anim-buffer',
734
+ label: 'Content Buffering',
735
+ description: 'Trim in-progress units so only complete words/lines reveal.',
736
+ type: 'select',
737
+ path: 'features.streamAnimation.buffer',
738
+ defaultValue: 'none',
739
+ options: [
740
+ { value: 'none', label: 'None — stream every character' },
741
+ { value: 'word', label: 'Word — hold until whitespace' },
742
+ { value: 'line', label: 'Line — hold until newline' },
743
+ ],
744
+ },
745
+ {
746
+ id: 'stream-anim-speed',
747
+ label: 'Per-unit Duration (ms)',
748
+ description: 'Animation length for each character or word.',
749
+ type: 'select',
750
+ path: 'features.streamAnimation.speed',
751
+ defaultValue: 120,
752
+ options: [
753
+ { value: '40', label: '40ms — snappy' },
754
+ { value: '80', label: '80ms' },
755
+ { value: '120', label: '120ms (default)' },
756
+ { value: '200', label: '200ms' },
757
+ { value: '320', label: '320ms' },
758
+ { value: '480', label: '480ms — slow' },
759
+ ],
760
+ formatValue: (v: unknown) => String(v ?? 120),
761
+ parseValue: (v: unknown) => Number(v),
762
+ },
763
+ {
764
+ id: 'stream-anim-duration',
765
+ label: 'Container Duration (ms)',
766
+ description: 'Length of container-level effects (pop-bubble, custom plugins).',
767
+ type: 'select',
768
+ path: 'features.streamAnimation.duration',
769
+ defaultValue: 1800,
770
+ options: [
771
+ { value: '600', label: '600ms' },
772
+ { value: '1200', label: '1200ms' },
773
+ { value: '1800', label: '1800ms (default)' },
774
+ { value: '2400', label: '2400ms' },
775
+ { value: '3600', label: '3600ms — slow' },
776
+ ],
777
+ formatValue: (v: unknown) => String(v ?? 1800),
778
+ parseValue: (v: unknown) => Number(v),
779
+ },
780
+ ],
781
+ };
782
+
700
783
  const attachmentsSectionDef: SectionDef = {
701
784
  id: 'attachments-config', title: 'Attachments', collapsed: true,
702
785
  fields: [
@@ -773,7 +856,7 @@ export const CONFIGURE_SUB_GROUPS: SubGroupDef[] = [
773
856
  { label: 'Content', sections: [copySectionDef, suggestionsSectionDef] },
774
857
  { label: 'Layout', sections: [generalLayoutSectionDef, headerLayoutSectionDef, messagesLayoutSectionDef, messageActionsSectionDef] },
775
858
  { label: 'Widget', sections: [launcherBasicsSectionDef, launcherAdvancedSectionDef, sendButtonSectionDef, closeButtonSectionDef, clearChatSectionDef, statusIndicatorSectionDef] },
776
- { label: 'Features', sections: [featuresSectionDef, attachmentsSectionDef, artifactsSectionDef, artifactCustomizationSectionDef] },
859
+ { label: 'Features', sections: [featuresSectionDef, streamAnimationSectionDef, attachmentsSectionDef, artifactsSectionDef, artifactCustomizationSectionDef] },
777
860
  { label: 'Developer', collapsedByDefault: true, sections: [apiIntegrationSectionDef, debugSectionDef, markdownSectionDef] },
778
861
  ];
779
862
 
@@ -237,6 +237,19 @@ export interface MessageTokens {
237
237
  border?: TokenReference<'color'>;
238
238
  }
239
239
 
240
+ /**
241
+ * Welcome / intro card rendered above the message list when no messages exist.
242
+ * Set `copy.showWelcomeCard: false` to hide it; use `layout.slots["body-top"]`
243
+ * to replace it wholesale.
244
+ */
245
+ export interface IntroCardTokens extends ComponentTokenSet {
246
+ background?: TokenReference<'color'>;
247
+ borderRadius?: TokenReference<'radius'>;
248
+ padding?: TokenReference<'spacing'>;
249
+ /** Box-shadow on the intro card (token ref or raw CSS, e.g. `none`). */
250
+ shadow?: string;
251
+ }
252
+
240
253
  /** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
241
254
  export interface CollapsibleWidgetTokens {
242
255
  /** Background for content areas. */
@@ -449,6 +462,8 @@ export interface ComponentTokens {
449
462
  panel: PanelTokens;
450
463
  header: HeaderTokens;
451
464
  message: MessageTokens;
465
+ /** Welcome / intro card shown above the message list. */
466
+ introCard?: IntroCardTokens;
452
467
  /** Markdown surfaces (chat + artifact pane). */
453
468
  markdown?: MarkdownTokens;
454
469
  voice: VoiceTokens;