@runtypelabs/persona 3.5.2 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.cjs +46 -46
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +44 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.global.js +70 -70
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +46 -46
  8. package/dist/index.js.map +1 -1
  9. package/dist/theme-editor.cjs +18015 -0
  10. package/dist/theme-editor.d.cts +3888 -0
  11. package/dist/theme-editor.d.ts +3888 -0
  12. package/dist/theme-editor.js +17909 -0
  13. package/dist/theme-reference.cjs +1 -1
  14. package/dist/theme-reference.d.cts +33 -0
  15. package/dist/theme-reference.d.ts +33 -0
  16. package/dist/theme-reference.js +1 -1
  17. package/dist/widget.css +69 -25
  18. package/package.json +9 -7
  19. package/src/components/artifact-card.ts +1 -1
  20. package/src/components/composer-builder.ts +16 -29
  21. package/src/components/demo-carousel.ts +5 -5
  22. package/src/components/event-stream-view.test.ts +142 -0
  23. package/src/components/event-stream-view.ts +68 -29
  24. package/src/components/header-builder.ts +2 -2
  25. package/src/components/launcher.ts +9 -0
  26. package/src/components/message-bubble.ts +9 -3
  27. package/src/components/suggestions.ts +1 -1
  28. package/src/defaults.ts +24 -9
  29. package/src/scroll-to-bottom-defaults.test.ts +13 -0
  30. package/src/styles/widget.css +69 -25
  31. package/src/theme-editor/color-utils.ts +252 -0
  32. package/src/theme-editor/index.ts +131 -0
  33. package/src/theme-editor/presets.ts +144 -0
  34. package/src/theme-editor/preview-utils.ts +265 -0
  35. package/src/theme-editor/preview.ts +445 -0
  36. package/src/theme-editor/role-mappings.ts +343 -0
  37. package/src/theme-editor/sections.test.ts +43 -0
  38. package/src/theme-editor/sections.ts +994 -0
  39. package/src/theme-editor/state.ts +298 -0
  40. package/src/theme-editor/types.ts +177 -0
  41. package/src/theme-editor.ts +2 -0
  42. package/src/theme-reference.ts +8 -0
  43. package/src/types/theme.ts +11 -0
  44. package/src/types.ts +22 -0
  45. package/src/ui.scroll.test.ts +554 -0
  46. package/src/ui.ts +223 -133
  47. package/src/utils/auto-follow.test.ts +110 -0
  48. package/src/utils/auto-follow.ts +112 -0
  49. package/src/utils/plugins.ts +1 -1
  50. package/src/utils/theme.test.ts +44 -8
  51. package/src/utils/theme.ts +11 -11
  52. package/src/utils/tokens.ts +137 -41
  53. package/widget.css +0 -1
@@ -0,0 +1,298 @@
1
+ /** Headless state management for the theme editor (no DOM, no localStorage, no side effects) */
2
+
3
+ import type { AgentWidgetConfig } from '../types';
4
+ import type { PersonaTheme } from '../types/theme';
5
+ import { createTheme } from '../utils/theme';
6
+ import { DEFAULT_WIDGET_CONFIG } from '../defaults';
7
+ import type { ConfiguratorSnapshot, ConfigChangeListener } from './types';
8
+
9
+ // ─── Dot-path utilities ─────────────────────────────────────────
10
+
11
+ function getByPath(obj: unknown, path: string): unknown {
12
+ const parts = path.split('.');
13
+ let current: unknown = obj;
14
+ for (const part of parts) {
15
+ if (current === undefined || current === null) return undefined;
16
+ current = (current as Record<string, unknown>)[part];
17
+ }
18
+ return current;
19
+ }
20
+
21
+ function setByPath(obj: unknown, path: string, value: unknown): unknown {
22
+ const parts = path.split('.');
23
+ if (parts.length === 1) {
24
+ return { ...(obj as Record<string, unknown>), [parts[0]]: value };
25
+ }
26
+
27
+ const [first, ...rest] = parts;
28
+ const current = obj as Record<string, unknown>;
29
+ return {
30
+ ...current,
31
+ [first]: setByPath(current?.[first] ?? {}, rest.join('.'), value),
32
+ };
33
+ }
34
+
35
+ // ─── ThemeEditorState ───────────────────────────────────────────
36
+
37
+ export class ThemeEditorState {
38
+ private config: AgentWidgetConfig;
39
+ private theme: PersonaTheme;
40
+ private listeners: ConfigChangeListener[] = [];
41
+ private history: ConfiguratorSnapshot[] = [];
42
+ private historyIndex = -1;
43
+ private suppressHistory = false;
44
+
45
+ constructor(
46
+ initialTheme?: Partial<PersonaTheme>,
47
+ initialConfig?: Partial<AgentWidgetConfig>,
48
+ options?: { mergeDefaults?: boolean }
49
+ ) {
50
+ const mergeDefaults = options?.mergeDefaults ?? true;
51
+ this.config = (mergeDefaults
52
+ ? { ...DEFAULT_WIDGET_CONFIG, ...initialConfig }
53
+ : (initialConfig ?? DEFAULT_WIDGET_CONFIG)
54
+ ) as AgentWidgetConfig;
55
+ this.theme = createTheme(initialTheme, { validate: false });
56
+ this.syncThemeIntoConfig();
57
+ this.pushHistorySnapshot(this.exportSnapshot(), true);
58
+ }
59
+
60
+ // ─── Read ───────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Get a value using a dot-path.
64
+ * - `theme.*` → reads from the PersonaTheme
65
+ * - `darkTheme.*` → reads from config.darkTheme
66
+ * - everything else → reads from the AgentWidgetConfig
67
+ */
68
+ get(path: string): unknown {
69
+ if (path.startsWith('theme.')) {
70
+ return getByPath(this.theme, path.replace('theme.', ''));
71
+ }
72
+ if (path.startsWith('darkTheme.')) {
73
+ return getByPath(this.config.darkTheme ?? {}, path.replace('darkTheme.', ''));
74
+ }
75
+ return getByPath(this.config, path);
76
+ }
77
+
78
+ getTheme(): PersonaTheme {
79
+ return this.theme;
80
+ }
81
+
82
+ getConfig(): AgentWidgetConfig {
83
+ return this.config;
84
+ }
85
+
86
+ // ─── Write ──────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Set a value using a dot-path.
90
+ * - `theme.*` → writes into the PersonaTheme
91
+ * - `darkTheme.*` → writes into config.darkTheme
92
+ * - everything else → writes into AgentWidgetConfig
93
+ */
94
+ set(path: string, value: unknown): void {
95
+ if (path.startsWith('theme.')) {
96
+ const themePath = path.replace('theme.', '');
97
+ this.theme = setByPath(this.theme, themePath, value) as PersonaTheme;
98
+ this.syncThemeIntoConfig();
99
+ } else if (path.startsWith('darkTheme.')) {
100
+ const themePath = path.replace('darkTheme.', '');
101
+ const dark = this.config.darkTheme ?? createTheme();
102
+ this.config = {
103
+ ...this.config,
104
+ darkTheme: setByPath(dark, themePath, value) as AgentWidgetConfig['darkTheme'],
105
+ };
106
+ } else {
107
+ this.config = setByPath(this.config, path, value) as AgentWidgetConfig;
108
+ }
109
+
110
+ this.recordHistory();
111
+ this.notifyListeners();
112
+ }
113
+
114
+ /** Batch-set multiple paths at once */
115
+ setBatch(updates: Record<string, unknown>): void {
116
+ let themeChanged = false;
117
+ let darkThemeChanged = false;
118
+ let configChanged = false;
119
+
120
+ for (const [path, value] of Object.entries(updates)) {
121
+ if (path.startsWith('theme.')) {
122
+ const themePath = path.replace('theme.', '');
123
+ this.theme = setByPath(this.theme, themePath, value) as PersonaTheme;
124
+ themeChanged = true;
125
+ } else if (path.startsWith('darkTheme.')) {
126
+ const themePath = path.replace('darkTheme.', '');
127
+ const dark = this.config.darkTheme ?? createTheme();
128
+ this.config = {
129
+ ...this.config,
130
+ darkTheme: setByPath(dark, themePath, value) as AgentWidgetConfig['darkTheme'],
131
+ };
132
+ darkThemeChanged = true;
133
+ } else {
134
+ this.config = setByPath(this.config, path, value) as AgentWidgetConfig;
135
+ configChanged = true;
136
+ }
137
+ }
138
+
139
+ if (themeChanged) {
140
+ this.syncThemeIntoConfig();
141
+ }
142
+ if (themeChanged || darkThemeChanged || configChanged) {
143
+ this.recordHistory();
144
+ this.notifyListeners();
145
+ }
146
+ }
147
+
148
+ /** Replace the entire theme */
149
+ setTheme(theme: PersonaTheme): void {
150
+ this.theme = theme;
151
+ this.syncThemeIntoConfig();
152
+ this.recordHistory();
153
+ this.notifyListeners();
154
+ }
155
+
156
+ /** Replace the entire config (for preset loading) */
157
+ setFullConfig(config: AgentWidgetConfig, theme?: PersonaTheme): void {
158
+ this.config = { ...config };
159
+ if (theme) {
160
+ this.theme = theme;
161
+ }
162
+ this.syncThemeIntoConfig();
163
+ this.recordHistory();
164
+ this.notifyListeners();
165
+ }
166
+
167
+ /** Import a snapshot (v2 or raw theme) */
168
+ importSnapshot(snapshot: unknown): void {
169
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
170
+ throw new Error('Snapshot must be a JSON object');
171
+ }
172
+
173
+ const parsed = snapshot as Partial<ConfiguratorSnapshot> & { config?: unknown; theme?: unknown };
174
+
175
+ if ('config' in parsed || 'theme' in parsed || parsed.version === 2) {
176
+ const config = (parsed.config ?? this.config) as AgentWidgetConfig;
177
+ const theme = createTheme(
178
+ (parsed.theme ?? this.theme) as Partial<PersonaTheme>,
179
+ { validate: false }
180
+ );
181
+ this.setFullConfig(config, theme);
182
+ return;
183
+ }
184
+
185
+ const theme = createTheme(parsed as Partial<PersonaTheme>, { validate: false });
186
+ this.setTheme(theme);
187
+ }
188
+
189
+ /** Reset to defaults */
190
+ resetToDefaults(): void {
191
+ this.config = { ...DEFAULT_WIDGET_CONFIG } as AgentWidgetConfig;
192
+ this.theme = createTheme();
193
+ this.syncThemeIntoConfig();
194
+ this.history = [];
195
+ this.historyIndex = -1;
196
+ this.pushHistorySnapshot(this.exportSnapshot());
197
+ this.notifyListeners();
198
+ }
199
+
200
+ // ─── History ────────────────────────────────────────────────
201
+
202
+ canUndo(): boolean {
203
+ return this.historyIndex > 0;
204
+ }
205
+
206
+ canRedo(): boolean {
207
+ return this.historyIndex >= 0 && this.historyIndex < this.history.length - 1;
208
+ }
209
+
210
+ getHistoryLength(): number {
211
+ return this.history.length;
212
+ }
213
+
214
+ getHistoryIndex(): number {
215
+ return this.historyIndex;
216
+ }
217
+
218
+ undo(): void {
219
+ if (!this.canUndo()) return;
220
+ this.historyIndex -= 1;
221
+ this.restoreSnapshot(this.history[this.historyIndex]);
222
+ }
223
+
224
+ redo(): void {
225
+ if (!this.canRedo()) return;
226
+ this.historyIndex += 1;
227
+ this.restoreSnapshot(this.history[this.historyIndex]);
228
+ }
229
+
230
+ // ─── Snapshots ──────────────────────────────────────────────
231
+
232
+ exportSnapshot(): ConfiguratorSnapshot {
233
+ return {
234
+ version: 2,
235
+ config: { ...this.config, theme: undefined } as unknown as Record<string, unknown>,
236
+ theme: this.theme,
237
+ };
238
+ }
239
+
240
+ // ─── Listeners ──────────────────────────────────────────────
241
+
242
+ onChange(listener: ConfigChangeListener): () => void {
243
+ this.listeners.push(listener);
244
+ return () => {
245
+ const idx = this.listeners.indexOf(listener);
246
+ if (idx >= 0) this.listeners.splice(idx, 1);
247
+ };
248
+ }
249
+
250
+ // ─── Private ────────────────────────────────────────────────
251
+
252
+ private syncThemeIntoConfig(): void {
253
+ this.config = {
254
+ ...this.config,
255
+ theme: this.theme,
256
+ };
257
+ }
258
+
259
+ private notifyListeners(): void {
260
+ for (const listener of this.listeners) {
261
+ listener(this.config, this.theme);
262
+ }
263
+ }
264
+
265
+ private recordHistory(): void {
266
+ this.pushHistorySnapshot(this.exportSnapshot());
267
+ }
268
+
269
+ private pushHistorySnapshot(snapshot: ConfiguratorSnapshot, replaceCurrent = false): void {
270
+ if (this.suppressHistory) return;
271
+
272
+ const serialized = JSON.stringify(snapshot);
273
+ const currentSerialized =
274
+ this.historyIndex >= 0 && this.history[this.historyIndex]
275
+ ? JSON.stringify(this.history[this.historyIndex])
276
+ : null;
277
+
278
+ if (replaceCurrent && this.historyIndex >= 0) {
279
+ this.history[this.historyIndex] = snapshot;
280
+ return;
281
+ }
282
+
283
+ if (serialized === currentSerialized) return;
284
+
285
+ this.history = this.history.slice(0, this.historyIndex + 1);
286
+ this.history.push(snapshot);
287
+ this.historyIndex = this.history.length - 1;
288
+ }
289
+
290
+ private restoreSnapshot(snapshot: ConfiguratorSnapshot): void {
291
+ this.suppressHistory = true;
292
+ this.config = snapshot.config as unknown as AgentWidgetConfig;
293
+ this.theme = createTheme(snapshot.theme, { validate: false });
294
+ this.syncThemeIntoConfig();
295
+ this.suppressHistory = false;
296
+ this.notifyListeners();
297
+ }
298
+ }
@@ -0,0 +1,177 @@
1
+ /** Field definition types for the declarative configurator system (headless — no DOM) */
2
+
3
+ import type { DeepPartial, PersonaTheme } from '../types/theme';
4
+ import type { AgentWidgetConfig } from '../types';
5
+
6
+ // ─── Field System ────────────────────────────────────────────────
7
+
8
+ export type FieldType =
9
+ | 'color'
10
+ | 'slider'
11
+ | 'toggle'
12
+ | 'select'
13
+ | 'text'
14
+ | 'chip-list'
15
+ | 'color-scale'
16
+ | 'token-ref'
17
+ | 'role-assignment';
18
+
19
+ export interface SliderOptions {
20
+ min: number;
21
+ max: number;
22
+ step: number;
23
+ unit?: 'px' | 'rem' | 'none';
24
+ /** Treat max value as 9999px (border-radius: full) */
25
+ isRadiusFull?: boolean;
26
+ }
27
+
28
+ export interface SelectOption {
29
+ value: string;
30
+ label: string;
31
+ }
32
+
33
+ export interface ColorScaleOptions {
34
+ /** Which palette color family (e.g., 'primary', 'gray') */
35
+ colorFamily: string;
36
+ }
37
+
38
+ export interface TokenRefOptions {
39
+ /** Token type to filter available references */
40
+ tokenType: 'color' | 'spacing' | 'radius' | 'shadow' | 'typography';
41
+ /** Available palette families to reference */
42
+ families?: string[];
43
+ }
44
+
45
+ // ─── Role Assignment System ─────────────────────────────────────
46
+
47
+ /** Which kind of value a role target expects */
48
+ export type RoleTargetKind = 'background' | 'foreground' | 'border' | 'accent';
49
+
50
+ /** A single token path that a role assignment writes to */
51
+ export interface RoleTarget {
52
+ /** Theme token path (e.g., 'components.message.user.background') */
53
+ path: string;
54
+ /** What kind of value this target expects */
55
+ kind: RoleTargetKind;
56
+ }
57
+
58
+ /** An intensity preset (e.g., Solid uses .500 shades, Soft uses .100 shades) */
59
+ export interface RoleIntensity {
60
+ id: string;
61
+ label: string;
62
+ }
63
+
64
+ /** Options for a role-assignment field */
65
+ export interface RoleAssignmentOptions {
66
+ /** Unique role identifier */
67
+ roleId: string;
68
+ /** Helper text shown below the role name */
69
+ helper: string;
70
+ /** Token paths this role writes to */
71
+ targets: RoleTarget[];
72
+ /** Available intensity presets */
73
+ intensities: RoleIntensity[];
74
+ /** Which data-persona-theme-zone this role corresponds to (for preview highlighting) */
75
+ previewZone?: string;
76
+ }
77
+
78
+ export interface FieldDef {
79
+ id: string;
80
+ label: string;
81
+ description?: string;
82
+ type: FieldType;
83
+ /** Dot-path into the config/theme object */
84
+ path: string;
85
+ defaultValue?: unknown;
86
+ /** Slider-specific options */
87
+ slider?: SliderOptions;
88
+ /** Select-specific options */
89
+ options?: SelectOption[];
90
+ /** Color-scale-specific options */
91
+ colorScale?: ColorScaleOptions;
92
+ /** Token-ref-specific options */
93
+ tokenRef?: TokenRefOptions;
94
+ /** Role-assignment-specific options */
95
+ roleAssignment?: RoleAssignmentOptions;
96
+ /** CSS property hint for value formatting */
97
+ cssProperty?: string;
98
+ /** Whether this is a theme path (vs config path) */
99
+ isThemePath?: boolean;
100
+ /** Convert stored value into a control-friendly value */
101
+ formatValue?: (value: unknown) => unknown;
102
+ /** Convert control input back into the stored value shape */
103
+ parseValue?: (value: unknown) => unknown;
104
+ }
105
+
106
+ export interface SectionDef {
107
+ id: string;
108
+ title: string;
109
+ description?: string;
110
+ fields: FieldDef[];
111
+ /** Whether the section starts collapsed */
112
+ collapsed?: boolean;
113
+ /** Preset buttons for this section */
114
+ presets?: SectionPreset[];
115
+ }
116
+
117
+ export interface SectionPreset {
118
+ id: string;
119
+ label: string;
120
+ values: Record<string, unknown>;
121
+ }
122
+
123
+ export interface TabDef {
124
+ id: string;
125
+ label: string;
126
+ icon?: string;
127
+ sections: SectionDef[];
128
+ }
129
+
130
+ export interface SubGroupDef {
131
+ label: string;
132
+ sections: SectionDef[];
133
+ /** When true, the sub-group starts collapsed and must be explicitly expanded */
134
+ collapsedByDefault?: boolean;
135
+ }
136
+
137
+ // ─── Preset System ───────────────────────────────────────────────
138
+
139
+ /** Extract the toolCall config type from AgentWidgetConfig */
140
+ type AgentWidgetToolCallConfig = NonNullable<AgentWidgetConfig['toolCall']>;
141
+
142
+ export interface ThemeEditorPreset {
143
+ id: string;
144
+ name: string;
145
+ description: string;
146
+ theme: DeepPartial<PersonaTheme>;
147
+ darkTheme?: DeepPartial<PersonaTheme>;
148
+ /** Tool call styling for light mode */
149
+ toolCall?: AgentWidgetToolCallConfig;
150
+ /** Tool call styling for dark mode (falls back to toolCall if not set) */
151
+ darkToolCall?: AgentWidgetToolCallConfig;
152
+ preview: {
153
+ primary: string;
154
+ surface: string;
155
+ accent: string;
156
+ };
157
+ darkPreview?: {
158
+ primary: string;
159
+ surface: string;
160
+ accent: string;
161
+ };
162
+ /** Tags for filtering/categorization */
163
+ tags?: string[];
164
+ }
165
+
166
+ // ─── State ───────────────────────────────────────────────────────
167
+
168
+ export interface ConfiguratorSnapshot {
169
+ version: 2;
170
+ config: Record<string, unknown>;
171
+ theme: PersonaTheme;
172
+ }
173
+
174
+ export type ConfigChangeListener = (config: AgentWidgetConfig, theme: PersonaTheme) => void;
175
+
176
+ /** Callback for when a control value changes */
177
+ export type OnChangeCallback = (path: string, value: unknown) => void;
@@ -0,0 +1,2 @@
1
+ /** Entry point for @runtypelabs/persona/theme-editor */
2
+ export * from './theme-editor/index';
@@ -138,6 +138,8 @@ export const THEME_TOKEN_DOCS = {
138
138
  approval:
139
139
  'requested (background, border, text), approve (background, foreground), deny (background, foreground).',
140
140
  attachment: 'image (background, border).',
141
+ scrollToBottom:
142
+ 'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
141
143
  toolBubble: 'shadow — tool call row box-shadow.',
142
144
  reasoningBubble: 'shadow — reasoning/thinking row box-shadow.',
143
145
  composer: 'shadow — message input form box-shadow.',
@@ -201,6 +203,12 @@ export const THEME_TOKEN_DOCS = {
201
203
  properties:
202
204
  'enabled, iconColor, backgroundColor, borderWidth, borderColor, borderRadius, size.',
203
205
  },
206
+ scrollToBottom: {
207
+ description:
208
+ 'Shared transcript + event-stream jump-to-latest affordance.',
209
+ properties:
210
+ 'features.scrollToBottom.enabled, features.scrollToBottom.iconName, features.scrollToBottom.label (empty string renders icon-only). Defaults: enabled=true, iconName="arrow-down", label="".',
211
+ },
204
212
  toolCall: {
205
213
  description: 'Tool call display styling.',
206
214
  properties:
@@ -25,6 +25,7 @@ export interface ColorPalette {
25
25
  success: ColorShade;
26
26
  warning: ColorShade;
27
27
  error: ColorShade;
28
+ info: ColorShade;
28
29
  [key: string]: ColorShade;
29
30
  }
30
31
 
@@ -392,6 +393,14 @@ export interface LabelButtonTokens {
392
393
  gap?: string;
393
394
  }
394
395
 
396
+ /** Scroll-to-bottom pill chrome shared by transcript + event stream. */
397
+ export interface ScrollToBottomTokens extends ComponentTokenSet {
398
+ size?: string;
399
+ gap?: string;
400
+ fontSize?: string;
401
+ iconSize?: string;
402
+ }
403
+
395
404
  /** Toggle group chrome (used by createToggleGroup). */
396
405
  export interface ToggleGroupTokens {
397
406
  /** Gap between toggle buttons. Default: 0 (connected). */
@@ -419,6 +428,8 @@ export interface ComponentTokens {
419
428
  iconButton?: IconButtonTokens;
420
429
  /** Label button styling tokens. */
421
430
  labelButton?: LabelButtonTokens;
431
+ /** Scroll-to-bottom indicator styling tokens. */
432
+ scrollToBottom?: ScrollToBottomTokens;
422
433
  /** Toggle group styling tokens. */
423
434
  toggleGroup?: ToggleGroupTokens;
424
435
  /** Artifact toolbar, tab strip, and pane chrome. */
package/src/types.ts CHANGED
@@ -552,10 +552,32 @@ export type AgentWidgetArtifactsFeature = {
552
552
  }) => HTMLElement | null;
553
553
  };
554
554
 
555
+ export type AgentWidgetScrollToBottomFeature = {
556
+ /**
557
+ * When true, Persona shows a scroll-to-bottom affordance when the user breaks
558
+ * away from the latest transcript or event stream content.
559
+ * @default true
560
+ */
561
+ enabled?: boolean;
562
+ /**
563
+ * Lucide icon name used for the affordance.
564
+ * @default "arrow-down"
565
+ */
566
+ iconName?: string;
567
+ /**
568
+ * Optional label text shown next to the icon. Set to an empty string for an
569
+ * icon-only affordance.
570
+ * @default ""
571
+ */
572
+ label?: string;
573
+ };
574
+
555
575
  export type AgentWidgetFeatureFlags = {
556
576
  showReasoning?: boolean;
557
577
  showToolCalls?: boolean;
558
578
  showEventStreamToggle?: boolean;
579
+ /** Shared transcript + event stream scroll-to-bottom affordance. */
580
+ scrollToBottom?: AgentWidgetScrollToBottomFeature;
559
581
  /** Configuration for the Event Stream inspector view */
560
582
  eventStream?: EventStreamConfig;
561
583
  /** Optional artifact sidebar (split pane / mobile drawer) */