@runtypelabs/persona 4.13.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 (106) 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 +339 -22
  14. package/dist/context-mentions.d.ts +339 -22
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +68 -67
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +483 -48
  19. package/dist/index.d.ts +483 -48
  20. package/dist/index.global.js +61 -58
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +70 -69
  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 +339 -22
  27. package/dist/smart-dom-reader.d.ts +339 -22
  28. package/dist/theme-editor-preview.cjs +67 -67
  29. package/dist/theme-editor-preview.d.cts +355 -23
  30. package/dist/theme-editor-preview.d.ts +355 -23
  31. package/dist/theme-editor-preview.js +75 -75
  32. package/dist/theme-editor.cjs +10 -10
  33. package/dist/theme-editor.d.cts +376 -24
  34. package/dist/theme-editor.d.ts +376 -24
  35. package/dist/theme-editor.js +10 -10
  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/artifacts-session.test.ts +106 -0
  43. package/src/ask-user-question-tool.ts +10 -7
  44. package/src/client.test.ts +2 -2
  45. package/src/client.ts +4 -1
  46. package/src/components/artifact-card.ts +13 -2
  47. package/src/components/artifact-inline.ts +11 -2
  48. package/src/components/artifact-preview.ts +1 -1
  49. package/src/components/composer-builder.test.ts +2 -0
  50. package/src/components/composer-builder.ts +1 -1
  51. package/src/components/composer-parts.test.ts +105 -4
  52. package/src/components/composer-parts.ts +54 -16
  53. package/src/components/message-bubble.test.ts +7 -1
  54. package/src/components/message-bubble.ts +11 -8
  55. package/src/components/messages.ts +5 -2
  56. package/src/components/panel.ts +31 -5
  57. package/src/components/pill-composer-builder.test.ts +18 -0
  58. package/src/components/suggestions.test.ts +233 -0
  59. package/src/components/suggestions.ts +463 -81
  60. package/src/components/widget-view.ts +4 -0
  61. package/src/defaults.ts +5 -3
  62. package/src/generated/runtype-openapi-contract.ts +17 -0
  63. package/src/index-core.ts +52 -2
  64. package/src/launcher-global.ts +1 -1
  65. package/src/plugins/types.ts +71 -3
  66. package/src/presets.ts +3 -3
  67. package/src/session.ts +55 -12
  68. package/src/styles/widget-styles.test.ts +162 -0
  69. package/src/styles/widget.css +440 -1
  70. package/src/suggest-replies-tool.test.ts +233 -2
  71. package/src/suggest-replies-tool.ts +115 -39
  72. package/src/theme-editor/index.ts +5 -0
  73. package/src/theme-editor/presets.test.ts +43 -0
  74. package/src/theme-editor/presets.ts +101 -87
  75. package/src/theme-editor/preview-utils.test.ts +34 -0
  76. package/src/theme-editor/preview-utils.ts +77 -2
  77. package/src/theme-editor/sections.test.ts +72 -7
  78. package/src/theme-editor/sections.ts +113 -13
  79. package/src/theme-editor/state-preferences.test.ts +15 -0
  80. package/src/theme-editor/state.ts +46 -0
  81. package/src/theme-editor/types.ts +15 -1
  82. package/src/theme-editor/webmcp/tools.test.ts +13 -0
  83. package/src/theme-editor/webmcp/tools.ts +5 -0
  84. package/src/theme-reference.ts +9 -2
  85. package/src/types/theme.ts +32 -0
  86. package/src/types.ts +291 -25
  87. package/src/ui.artifact-pane-gating.test.ts +5 -5
  88. package/src/ui.config-preferences.test.ts +166 -0
  89. package/src/ui.detached-panel.test.ts +1 -1
  90. package/src/ui.suggest-replies.test.ts +1066 -1
  91. package/src/ui.ts +311 -45
  92. package/src/ui.typing-indicator.test.ts +135 -0
  93. package/src/utils/artifact-display.test.ts +180 -16
  94. package/src/utils/artifact-display.ts +155 -14
  95. package/src/utils/artifact-file.test.ts +1 -1
  96. package/src/utils/code-generators.test.ts +40 -0
  97. package/src/utils/code-generators.ts +22 -0
  98. package/src/utils/config-merge.test.ts +84 -0
  99. package/src/utils/config-merge.ts +6 -1
  100. package/src/utils/feature-preferences.test.ts +326 -0
  101. package/src/utils/feature-preferences.ts +498 -0
  102. package/src/utils/icons.test.ts +26 -0
  103. package/src/utils/icons.ts +8 -0
  104. package/src/utils/theme.test.ts +171 -0
  105. package/src/utils/tokens.ts +71 -7
  106. package/dist/chunk-MMUPR2JW.js +0 -1
@@ -0,0 +1,498 @@
1
+ import type {
2
+ AgentWidgetArtifactsFeature,
3
+ AgentWidgetConfig,
4
+ AgentWidgetFeatureFlags,
5
+ PersonaArtifactDisplayMode,
6
+ PersonaArtifactDisplayModeInput,
7
+ PersonaArtifactDisplayRules,
8
+ PersonaArtifactFilesDisplayRules,
9
+ PersonaArtifactKind,
10
+ WidgetArtifactLayoutPreference,
11
+ WidgetPreferenceSlice,
12
+ } from "../types";
13
+ import {
14
+ canonicalArtifactDisplayMode,
15
+ normalizeMediaType,
16
+ } from "./artifact-display";
17
+
18
+ export { normalizeMediaType };
19
+
20
+ export type WidgetPreferenceParseIssueCode =
21
+ | "invalid_type"
22
+ | "invalid_value"
23
+ | "unknown_key"
24
+ | "deprecated_key";
25
+
26
+ export type WidgetPreferenceParseIssue = {
27
+ path: string;
28
+ code: WidgetPreferenceParseIssueCode;
29
+ message: string;
30
+ };
31
+
32
+ export type WidgetPreferenceParseResult = {
33
+ preferences: WidgetPreferenceSlice;
34
+ issues: WidgetPreferenceParseIssue[];
35
+ };
36
+
37
+ const BLOCKED_KEYS = new Set(["__proto__", "prototype", "constructor"]);
38
+ // Accepted input values; "card" is the deprecated alias of "collapsed" and
39
+ // parsing canonicalizes it.
40
+ const DISPLAY_MODES = new Set<PersonaArtifactDisplayModeInput>([
41
+ "collapsed",
42
+ "card",
43
+ "panel",
44
+ "inline",
45
+ ]);
46
+ const ARTIFACT_KINDS = new Set<PersonaArtifactKind>([
47
+ "markdown",
48
+ "component",
49
+ ]);
50
+ // Exact "type/subtype" or major-type wildcard "type/*"; "*/*" is rejected
51
+ // because it duplicates files.default.
52
+ const MEDIA_TYPE_SELECTOR_PATTERN =
53
+ /^[a-z0-9][a-z0-9!#$&^_.+-]*\/(\*|[a-z0-9!#$&^_.+-]+)$/;
54
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
55
+ typeof value === "object" && value !== null && !Array.isArray(value);
56
+
57
+ const isSafeKey = (key: string): boolean => !BLOCKED_KEYS.has(key);
58
+
59
+ const isDisplayMode = (
60
+ value: unknown
61
+ ): value is PersonaArtifactDisplayModeInput =>
62
+ typeof value === "string" &&
63
+ DISPLAY_MODES.has(value as PersonaArtifactDisplayModeInput);
64
+
65
+ const hasKeys = (value: object): boolean => Object.keys(value).length > 0;
66
+
67
+ function issue(
68
+ issues: WidgetPreferenceParseIssue[],
69
+ path: string,
70
+ code: WidgetPreferenceParseIssueCode,
71
+ message: string
72
+ ): undefined {
73
+ issues.push({ path, code, message });
74
+ return undefined;
75
+ }
76
+
77
+ const UNKNOWN_KEY_MESSAGE =
78
+ "Ignored because this key is not part of the persisted preference schema.";
79
+ const UNSUPPORTED_VALUE_MESSAGE = "Ignored an unsupported value.";
80
+ const MODE_VALUE_MESSAGE = "Expected collapsed, panel, or inline.";
81
+ const MEDIA_TYPE_MESSAGE =
82
+ 'Expected a MIME type such as "text/html" or a wildcard such as "image/*".';
83
+
84
+ type PreferenceNode = {
85
+ /** Fold this key's parsed value into another output key, that key winning. */
86
+ alias?: string;
87
+ /** Reported whenever the input carries this key, valid or not. */
88
+ deprecated?: string;
89
+ } & (
90
+ | { kind: "boolean" }
91
+ | { kind: "string"; message: string }
92
+ | { kind: "enum"; values: ReadonlySet<string>; message: string }
93
+ /** Display mode; canonicalizes the deprecated "card" alias to "collapsed". */
94
+ | { kind: "mode"; message: string }
95
+ /** Mode values keyed by a closed selector set, or by MIME selector. */
96
+ | { kind: "modeMap"; keys: ReadonlySet<string> | "mediaType" }
97
+ | {
98
+ kind: "object";
99
+ children: Record<string, PreferenceNode>;
100
+ message?: string;
101
+ /** Also accept a bare mode string in place of the rules object. */
102
+ bareMode?: true;
103
+ }
104
+ );
105
+
106
+ const DEFAULT_MODE: PreferenceNode = {
107
+ kind: "mode",
108
+ message: UNSUPPORTED_VALUE_MESSAGE,
109
+ };
110
+ const CSS_LENGTH: PreferenceNode = {
111
+ kind: "string",
112
+ message: "Expected a CSS length string.",
113
+ };
114
+ const BOOLEAN: PreferenceNode = { kind: "boolean" };
115
+
116
+ // Keyed by the preference type so the table stays exhaustive; string keys are
117
+ // declared before boolean keys because issue order follows declaration order.
118
+ const ARTIFACT_LAYOUT_CHILDREN: Record<
119
+ keyof WidgetArtifactLayoutPreference,
120
+ PreferenceNode
121
+ > = {
122
+ paneWidth: CSS_LENGTH,
123
+ paneMaxWidth: CSS_LENGTH,
124
+ paneMinWidth: CSS_LENGTH,
125
+ expandedPanelWidth: CSS_LENGTH,
126
+ resizableMinWidth: CSS_LENGTH,
127
+ resizableMaxWidth: CSS_LENGTH,
128
+ expandLauncherPanelWhenOpen: BOOLEAN,
129
+ resizable: BOOLEAN,
130
+ };
131
+
132
+ const ROOT_NODE: PreferenceNode = {
133
+ kind: "object",
134
+ message: "Expected a preference object.",
135
+ children: {
136
+ showToolCalls: BOOLEAN,
137
+ showReasoning: BOOLEAN,
138
+ toolCallDisplay: {
139
+ kind: "object",
140
+ children: {
141
+ collapsedMode: {
142
+ kind: "enum",
143
+ values: new Set(["tool-call", "tool-name", "tool-preview"]),
144
+ message: UNSUPPORTED_VALUE_MESSAGE,
145
+ },
146
+ groupedMode: {
147
+ kind: "enum",
148
+ values: new Set(["stack", "summary"]),
149
+ message: UNSUPPORTED_VALUE_MESSAGE,
150
+ },
151
+ loadingAnimation: {
152
+ kind: "enum",
153
+ values: new Set([
154
+ "none",
155
+ "pulse",
156
+ "shimmer",
157
+ "shimmer-color",
158
+ "rainbow",
159
+ ]),
160
+ message: UNSUPPORTED_VALUE_MESSAGE,
161
+ },
162
+ expandable: BOOLEAN,
163
+ grouped: BOOLEAN,
164
+ },
165
+ },
166
+ reasoningDisplay: {
167
+ kind: "object",
168
+ children: { expandable: BOOLEAN, activePreview: BOOLEAN },
169
+ },
170
+ artifacts: {
171
+ kind: "object",
172
+ children: {
173
+ display: {
174
+ kind: "object",
175
+ bareMode: true,
176
+ message: "Expected an artifact display rules object.",
177
+ children: {
178
+ default: DEFAULT_MODE,
179
+ files: {
180
+ kind: "object",
181
+ bareMode: true,
182
+ message:
183
+ "Expected collapsed, panel, inline, or a file display rules object.",
184
+ children: {
185
+ default: DEFAULT_MODE,
186
+ byMediaType: { kind: "modeMap", keys: "mediaType" },
187
+ },
188
+ },
189
+ // Declared before byKind: fold order decides alias precedence.
190
+ byType: {
191
+ kind: "modeMap",
192
+ keys: ARTIFACT_KINDS,
193
+ alias: "byKind",
194
+ deprecated:
195
+ "Use byKind. The legacy selector was normalized automatically.",
196
+ },
197
+ byKind: { kind: "modeMap", keys: ARTIFACT_KINDS },
198
+ },
199
+ },
200
+ filePreview: { kind: "object", children: { enabled: BOOLEAN } },
201
+ layout: { kind: "object", children: ARTIFACT_LAYOUT_CHILDREN },
202
+ },
203
+ },
204
+ },
205
+ };
206
+
207
+ /**
208
+ * Walk one value against its schema node. Returns the parsed value, or
209
+ * `undefined` when the value is absent, rejected, or parsed to nothing.
210
+ */
211
+ function parseNode(
212
+ value: unknown,
213
+ node: PreferenceNode,
214
+ path: string,
215
+ issues: WidgetPreferenceParseIssue[]
216
+ ): unknown {
217
+ if (value === undefined) return undefined;
218
+ switch (node.kind) {
219
+ case "boolean":
220
+ return typeof value === "boolean"
221
+ ? value
222
+ : issue(issues, path, "invalid_type", "Expected a boolean.");
223
+ case "string":
224
+ return typeof value === "string"
225
+ ? value
226
+ : issue(issues, path, "invalid_type", node.message);
227
+ case "enum":
228
+ return typeof value === "string" && node.values.has(value)
229
+ ? value
230
+ : issue(issues, path, "invalid_value", node.message);
231
+ case "mode":
232
+ return isDisplayMode(value)
233
+ ? canonicalArtifactDisplayMode(value)
234
+ : issue(issues, path, "invalid_value", node.message);
235
+ case "modeMap": {
236
+ if (!isRecord(value)) {
237
+ return issue(issues, path, "invalid_type", "Expected an object.");
238
+ }
239
+ const keys = node.keys === "mediaType" ? undefined : node.keys;
240
+ const map: Record<string, PersonaArtifactDisplayMode> = {};
241
+ for (const [key, mode] of Object.entries(value)) {
242
+ const selector = keys ? key : normalizeMediaType(key);
243
+ const known =
244
+ isSafeKey(key) &&
245
+ (keys ? keys.has(key) : MEDIA_TYPE_SELECTOR_PATTERN.test(selector));
246
+ if (!known) {
247
+ issue(
248
+ issues,
249
+ `${path}.${key}`,
250
+ keys ? "unknown_key" : "invalid_value",
251
+ keys ? "Ignored an unsupported selector." : MEDIA_TYPE_MESSAGE
252
+ );
253
+ } else if (!isDisplayMode(mode)) {
254
+ issue(issues, `${path}.${key}`, "invalid_value", MODE_VALUE_MESSAGE);
255
+ } else {
256
+ map[selector] = canonicalArtifactDisplayMode(mode);
257
+ }
258
+ }
259
+ return hasKeys(map) ? map : undefined;
260
+ }
261
+ case "object": {
262
+ // A bare mode is a meaningful stored value: when layers merge it replaces
263
+ // every lower-layer display rule instead of refining them.
264
+ if (node.bareMode && isDisplayMode(value)) {
265
+ return canonicalArtifactDisplayMode(value);
266
+ }
267
+ if (!isRecord(value)) {
268
+ return issue(
269
+ issues,
270
+ path,
271
+ "invalid_type",
272
+ node.message ?? "Expected an object."
273
+ );
274
+ }
275
+ const childKeys = Object.keys(node.children);
276
+ const childPath = (key: string) => (path ? `${path}.${key}` : key);
277
+ for (const key of Object.keys(value)) {
278
+ if (!isSafeKey(key) || !childKeys.includes(key)) {
279
+ issue(issues, childPath(key), "unknown_key", UNKNOWN_KEY_MESSAGE);
280
+ }
281
+ }
282
+ const result: Record<string, unknown> = {};
283
+ for (const key of childKeys) {
284
+ const child = node.children[key];
285
+ const parsed = parseNode(value[key], child, childPath(key), issues);
286
+ if (child.deprecated && value[key] !== undefined) {
287
+ issue(issues, childPath(key), "deprecated_key", child.deprecated);
288
+ }
289
+ if (parsed === undefined) continue;
290
+ const target = child.alias ?? key;
291
+ const previous = result[target];
292
+ result[target] =
293
+ isRecord(previous) && isRecord(parsed)
294
+ ? { ...previous, ...parsed }
295
+ : parsed;
296
+ }
297
+ return hasKeys(result) ? result : undefined;
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Parse untrusted JSON into Persona's curated preference schema.
304
+ *
305
+ * Unknown, invalid, capability, callback, renderer, and security fields are
306
+ * omitted. The returned issues are suitable for logging or a settings UI.
307
+ */
308
+ export function parseWidgetPreferenceSlice(
309
+ input: unknown
310
+ ): WidgetPreferenceParseResult {
311
+ const issues: WidgetPreferenceParseIssue[] = [];
312
+ // null is an absent slice, not a malformed one.
313
+ const parsed =
314
+ input === null ? undefined : parseNode(input, ROOT_NODE, "", issues);
315
+ return {
316
+ preferences: (parsed as WidgetPreferenceSlice | undefined) ?? {},
317
+ issues,
318
+ };
319
+ }
320
+
321
+ function cloneValue<T>(value: T): T {
322
+ if (Array.isArray(value)) return value.map(cloneValue) as T;
323
+ if (!isRecord(value)) return value;
324
+ const result: Record<string, unknown> = {};
325
+ for (const [key, child] of Object.entries(value)) {
326
+ if (isSafeKey(key) && child !== undefined) result[key] = cloneValue(child);
327
+ }
328
+ return result as T;
329
+ }
330
+
331
+ function mergeRecords(
332
+ lower: Record<string, unknown>,
333
+ upper: Record<string, unknown>
334
+ ): Record<string, unknown> {
335
+ const result = cloneValue(lower);
336
+ for (const [key, value] of Object.entries(upper)) {
337
+ if (!isSafeKey(key) || value === undefined) continue;
338
+ const previous = result[key];
339
+ result[key] =
340
+ isRecord(previous) && isRecord(value)
341
+ ? mergeRecords(previous, value)
342
+ : cloneValue(value);
343
+ }
344
+ return result;
345
+ }
346
+
347
+ /**
348
+ * Merge lowest-to-highest preference slices after parsing each as untrusted
349
+ * data. Objects merge per key, so later slices win only for keys they
350
+ * explicitly contain; a string `display` or `display.files` value in a later
351
+ * slice replaces that whole subtree instead.
352
+ */
353
+ export function mergeFeaturePreferences(
354
+ layers: readonly WidgetPreferenceSlice[]
355
+ ): WidgetPreferenceSlice {
356
+ let merged: Record<string, unknown> = {};
357
+ let display: ArtifactDisplayValue | undefined;
358
+ for (const layer of layers) {
359
+ const parsed = parseWidgetPreferenceSlice(layer).preferences;
360
+ display = mergeDisplayValue(display, parsed.artifacts?.display);
361
+ merged = mergeRecords(merged, parsed as Record<string, unknown>);
362
+ }
363
+ const result = merged as WidgetPreferenceSlice;
364
+ // The generic record merge cannot express string-replaces-object, so the
365
+ // display subtree is recomputed with the display-aware fold.
366
+ if (result.artifacts) {
367
+ if (display !== undefined) result.artifacts.display = display;
368
+ else delete result.artifacts.display;
369
+ }
370
+ return result;
371
+ }
372
+
373
+ type ArtifactDisplayValue =
374
+ | PersonaArtifactDisplayModeInput
375
+ | PersonaArtifactDisplayRules;
376
+
377
+ type ArtifactFilesValue = PersonaArtifactDisplayRules["files"];
378
+
379
+ /**
380
+ * One merge rule at every level: an upper string replaces the whole subtree it
381
+ * names; an upper object refines the lower value per key (a lower string is
382
+ * refined as that subtree's `default`).
383
+ */
384
+ function mergeFilesValue(
385
+ lower: ArtifactFilesValue,
386
+ upper: ArtifactFilesValue
387
+ ): ArtifactFilesValue {
388
+ if (upper === undefined) {
389
+ return lower === undefined ? undefined : cloneValue(lower);
390
+ }
391
+ if (typeof upper === "string") return upper;
392
+ const low: PersonaArtifactFilesDisplayRules =
393
+ typeof lower === "string" ? { default: lower } : lower ?? {};
394
+ const defaultMode = upper.default ?? low.default;
395
+ const byMediaType = { ...low.byMediaType, ...upper.byMediaType };
396
+ const result: PersonaArtifactFilesDisplayRules = {
397
+ ...(defaultMode ? { default: defaultMode } : {}),
398
+ ...(hasKeys(byMediaType) ? { byMediaType } : {}),
399
+ };
400
+ return hasKeys(result) ? result : undefined;
401
+ }
402
+
403
+ function mergeDisplayValue(
404
+ lower: AgentWidgetArtifactsFeature["display"],
405
+ upper: AgentWidgetArtifactsFeature["display"]
406
+ ): ArtifactDisplayValue | undefined {
407
+ if (upper === undefined) {
408
+ return lower === undefined ? undefined : cloneValue(lower);
409
+ }
410
+ if (typeof upper === "string") return upper;
411
+ const low: PersonaArtifactDisplayRules =
412
+ typeof lower === "string" ? { default: lower } : lower ?? {};
413
+ const defaultMode = upper.default ?? low.default;
414
+ const byKind = {
415
+ ...low.byType,
416
+ ...low.byKind,
417
+ ...upper.byType,
418
+ ...upper.byKind,
419
+ };
420
+ const files = mergeFilesValue(low.files, upper.files);
421
+ const result: PersonaArtifactDisplayRules = {
422
+ ...(defaultMode ? { default: defaultMode } : {}),
423
+ ...(hasKeys(byKind) ? { byKind } : {}),
424
+ ...(files !== undefined ? { files } : {}),
425
+ };
426
+ return hasKeys(result) ? result : undefined;
427
+ }
428
+
429
+ /**
430
+ * Overlay persisted display preferences onto code-owned feature config.
431
+ * Only fields in `WidgetPreferenceSlice` are copied.
432
+ */
433
+ export function applyFeaturePreferences(
434
+ baseFeatures: AgentWidgetFeatureFlags = {},
435
+ layers: readonly WidgetPreferenceSlice[]
436
+ ): AgentWidgetFeatureFlags {
437
+ const preferences = mergeFeaturePreferences(layers);
438
+ const result: AgentWidgetFeatureFlags = { ...baseFeatures };
439
+ if (preferences.showToolCalls !== undefined) {
440
+ result.showToolCalls = preferences.showToolCalls;
441
+ }
442
+ if (preferences.showReasoning !== undefined) {
443
+ result.showReasoning = preferences.showReasoning;
444
+ }
445
+ if (preferences.toolCallDisplay) {
446
+ result.toolCallDisplay = {
447
+ ...baseFeatures.toolCallDisplay,
448
+ ...preferences.toolCallDisplay,
449
+ };
450
+ }
451
+ if (preferences.reasoningDisplay) {
452
+ result.reasoningDisplay = {
453
+ ...baseFeatures.reasoningDisplay,
454
+ ...preferences.reasoningDisplay,
455
+ };
456
+ }
457
+ if (preferences.artifacts) {
458
+ const baseArtifacts = baseFeatures.artifacts ?? {};
459
+ const artifacts: AgentWidgetArtifactsFeature = { ...baseArtifacts };
460
+ if (preferences.artifacts.display !== undefined) {
461
+ const display = mergeDisplayValue(
462
+ baseArtifacts.display,
463
+ preferences.artifacts.display
464
+ );
465
+ if (display !== undefined) artifacts.display = display;
466
+ }
467
+ if (preferences.artifacts.filePreview) {
468
+ artifacts.filePreview = {
469
+ ...baseArtifacts.filePreview,
470
+ enabled: preferences.artifacts.filePreview.enabled,
471
+ };
472
+ }
473
+ if (preferences.artifacts.layout) {
474
+ artifacts.layout = {
475
+ ...baseArtifacts.layout,
476
+ ...preferences.artifacts.layout,
477
+ };
478
+ }
479
+ result.artifacts = artifacts;
480
+ }
481
+ return result;
482
+ }
483
+
484
+ /**
485
+ * Bake `config.preferences` into `config.features` for one widget instance.
486
+ * Callers supporting live preference updates must re-resolve from the
487
+ * pre-preference base features, not from a previously resolved result.
488
+ */
489
+ export function resolveConfigPreferences(
490
+ config: AgentWidgetConfig
491
+ ): AgentWidgetConfig {
492
+ const preferences = config.preferences;
493
+ if (!preferences || !hasKeys(preferences)) return config;
494
+ return {
495
+ ...config,
496
+ features: applyFeaturePreferences(config.features ?? {}, [preferences]),
497
+ };
498
+ }
@@ -0,0 +1,26 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { renderLucideIcon } from "./icons";
6
+
7
+ describe("renderLucideIcon", () => {
8
+ it.each(["lightbulb", "chart-column", "image", "pen-line"] as const)(
9
+ "renders %s as an inline svg",
10
+ (name) => {
11
+ const svg = renderLucideIcon(name);
12
+ expect(svg).not.toBeNull();
13
+ expect(svg?.tagName.toLowerCase()).toBe("svg");
14
+ expect(svg?.getAttribute("viewBox")).toBe("0 0 24 24");
15
+ // Geometry, not an empty shell.
16
+ expect(svg!.childNodes.length).toBeGreaterThan(0);
17
+ }
18
+ );
19
+
20
+ it("returns null and warns for a name outside the registry", () => {
21
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
22
+ expect(renderLucideIcon("definitely-not-an-icon")).toBeNull();
23
+ expect(warn).toHaveBeenCalled();
24
+ warn.mockRestore();
25
+ });
26
+ });
@@ -70,6 +70,7 @@ import {
70
70
  Plus,
71
71
  Minus,
72
72
  Pencil,
73
+ PenLine,
73
74
  Trash,
74
75
  Trash2,
75
76
  Save,
@@ -117,12 +118,15 @@ import {
117
118
  Folder,
118
119
  FolderOpen,
119
120
  Files,
121
+ // ---------- Data ----------
122
+ ChartColumn,
120
123
  // ---------- Decorative ----------
121
124
  Sparkles,
122
125
  Zap,
123
126
  Sun,
124
127
  Moon,
125
128
  Flag,
129
+ Lightbulb,
126
130
  // ---------- Devices ----------
127
131
  Monitor,
128
132
  Smartphone,
@@ -220,6 +224,7 @@ const LUCIDE_ICONS = {
220
224
  "plus": Plus,
221
225
  "minus": Minus,
222
226
  "pencil": Pencil,
227
+ "pen-line": PenLine,
223
228
  "trash": Trash,
224
229
  "trash-2": Trash2,
225
230
  "save": Save,
@@ -267,12 +272,15 @@ const LUCIDE_ICONS = {
267
272
  "folder": Folder,
268
273
  "folder-open": FolderOpen,
269
274
  "files": Files,
275
+ // Data
276
+ "chart-column": ChartColumn,
270
277
  // Decorative
271
278
  "sparkles": Sparkles,
272
279
  "zap": Zap,
273
280
  "sun": Sun,
274
281
  "moon": Moon,
275
282
  "flag": Flag,
283
+ "lightbulb": Lightbulb,
276
284
  // Devices
277
285
  "monitor": Monitor,
278
286
  "smartphone": Smartphone,