@runtypelabs/persona 4.13.0 → 4.14.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 (64) hide show
  1. package/dist/codegen.cjs +1 -1
  2. package/dist/codegen.js +1 -1
  3. package/dist/context-mentions.d.cts +98 -6
  4. package/dist/context-mentions.d.ts +98 -6
  5. package/dist/index.cjs +64 -64
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +183 -8
  8. package/dist/index.d.ts +183 -8
  9. package/dist/index.global.js +54 -54
  10. package/dist/index.global.js.map +1 -1
  11. package/dist/index.js +67 -67
  12. package/dist/index.js.map +1 -1
  13. package/dist/launcher.global.js +1 -1
  14. package/dist/launcher.global.js.map +1 -1
  15. package/dist/smart-dom-reader.d.cts +98 -6
  16. package/dist/smart-dom-reader.d.ts +98 -6
  17. package/dist/theme-editor-preview.cjs +66 -66
  18. package/dist/theme-editor-preview.d.cts +100 -6
  19. package/dist/theme-editor-preview.d.ts +100 -6
  20. package/dist/theme-editor-preview.js +75 -75
  21. package/dist/theme-editor.cjs +10 -10
  22. package/dist/theme-editor.d.cts +134 -7
  23. package/dist/theme-editor.d.ts +134 -7
  24. package/dist/theme-editor.js +10 -10
  25. package/dist/theme-reference.cjs +1 -1
  26. package/dist/widget.css +1 -1
  27. package/package.json +1 -1
  28. package/src/artifacts-session.test.ts +106 -0
  29. package/src/client.test.ts +2 -2
  30. package/src/client.ts +4 -1
  31. package/src/components/artifact-card.ts +13 -2
  32. package/src/components/artifact-inline.ts +11 -2
  33. package/src/components/composer-builder.test.ts +2 -0
  34. package/src/components/composer-builder.ts +1 -1
  35. package/src/components/composer-parts.test.ts +2 -0
  36. package/src/components/composer-parts.ts +1 -1
  37. package/src/components/message-bubble.test.ts +7 -1
  38. package/src/components/message-bubble.ts +11 -8
  39. package/src/index-core.ts +34 -0
  40. package/src/session.ts +45 -4
  41. package/src/styles/widget.css +10 -1
  42. package/src/theme-editor/index.ts +5 -0
  43. package/src/theme-editor/presets.test.ts +43 -0
  44. package/src/theme-editor/presets.ts +101 -87
  45. package/src/theme-editor/sections.test.ts +39 -7
  46. package/src/theme-editor/sections.ts +60 -8
  47. package/src/theme-editor/state-preferences.test.ts +15 -0
  48. package/src/theme-editor/state.ts +46 -0
  49. package/src/theme-editor/types.ts +15 -1
  50. package/src/theme-editor/webmcp/tools.test.ts +13 -0
  51. package/src/theme-editor/webmcp/tools.ts +5 -0
  52. package/src/types.ts +139 -9
  53. package/src/ui.artifact-pane-gating.test.ts +5 -5
  54. package/src/ui.config-preferences.test.ts +166 -0
  55. package/src/ui.ts +43 -25
  56. package/src/ui.typing-indicator.test.ts +135 -0
  57. package/src/utils/artifact-display.test.ts +180 -16
  58. package/src/utils/artifact-display.ts +155 -14
  59. package/src/utils/config-merge.test.ts +60 -0
  60. package/src/utils/config-merge.ts +6 -1
  61. package/src/utils/feature-preferences.test.ts +326 -0
  62. package/src/utils/feature-preferences.ts +498 -0
  63. package/src/utils/theme.test.ts +92 -0
  64. package/src/utils/tokens.ts +7 -7
@@ -0,0 +1,326 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ applyFeaturePreferences,
4
+ mergeFeaturePreferences,
5
+ parseWidgetPreferenceSlice,
6
+ resolveConfigPreferences,
7
+ } from "./feature-preferences";
8
+ import type { AgentWidgetConfig, WidgetPreferenceSlice } from "../types";
9
+
10
+ describe("feature preferences", () => {
11
+ test("merges object display rules by selector key across layers", () => {
12
+ expect(
13
+ mergeFeaturePreferences([
14
+ {
15
+ showToolCalls: false,
16
+ artifacts: {
17
+ display: {
18
+ default: "collapsed",
19
+ byKind: { markdown: "panel" },
20
+ files: { byMediaType: { "text/html": "inline" } },
21
+ },
22
+ },
23
+ },
24
+ {
25
+ showToolCalls: undefined,
26
+ artifacts: {
27
+ display: {
28
+ files: { default: "inline" },
29
+ byKind: { component: "collapsed" },
30
+ },
31
+ },
32
+ },
33
+ ])
34
+ ).toEqual({
35
+ showToolCalls: false,
36
+ artifacts: {
37
+ display: {
38
+ default: "collapsed",
39
+ byKind: { markdown: "panel", component: "collapsed" },
40
+ files: {
41
+ default: "inline",
42
+ byMediaType: { "text/html": "inline" },
43
+ },
44
+ },
45
+ },
46
+ });
47
+ });
48
+
49
+ test("a later-layer string replaces the whole subtree it names", () => {
50
+ expect(
51
+ mergeFeaturePreferences([
52
+ {
53
+ artifacts: {
54
+ display: {
55
+ default: "collapsed",
56
+ files: { byMediaType: { "text/csv": "panel" } },
57
+ },
58
+ },
59
+ },
60
+ { artifacts: { display: { files: "inline" } } },
61
+ ])
62
+ ).toEqual({
63
+ artifacts: {
64
+ display: { default: "collapsed", files: "inline" },
65
+ },
66
+ });
67
+ expect(
68
+ mergeFeaturePreferences([
69
+ { artifacts: { display: { default: "collapsed", files: "panel" } } },
70
+ { artifacts: { display: "inline" } },
71
+ ])
72
+ ).toEqual({
73
+ artifacts: { display: "inline" },
74
+ });
75
+ });
76
+
77
+ test("canonicalizes the deprecated 'card' mode alias in stored preferences", () => {
78
+ expect(
79
+ parseWidgetPreferenceSlice({
80
+ artifacts: {
81
+ display: {
82
+ default: "card",
83
+ files: { byMediaType: { "text/csv": "card" } },
84
+ byKind: { component: "card" },
85
+ },
86
+ },
87
+ }).preferences
88
+ ).toEqual({
89
+ artifacts: {
90
+ display: {
91
+ default: "collapsed",
92
+ files: { byMediaType: { "text/csv": "collapsed" } },
93
+ byKind: { component: "collapsed" },
94
+ },
95
+ },
96
+ });
97
+ });
98
+
99
+ test("normalizes the deprecated byType selector to byKind", () => {
100
+ expect(
101
+ parseWidgetPreferenceSlice({
102
+ artifacts: {
103
+ display: {
104
+ byType: { markdown: "collapsed", component: "panel" },
105
+ byKind: { component: "inline" },
106
+ },
107
+ },
108
+ }).preferences
109
+ ).toEqual({
110
+ artifacts: {
111
+ display: {
112
+ byKind: { markdown: "collapsed", component: "inline" },
113
+ },
114
+ },
115
+ });
116
+ });
117
+
118
+ test("does not mutate inputs and ignores prototype-pollution keys", () => {
119
+ const base = {
120
+ artifacts: {
121
+ display: {
122
+ byKind: { markdown: "collapsed" as const },
123
+ },
124
+ },
125
+ };
126
+ const unsafe = JSON.parse(
127
+ '{"artifacts":{"display":{"byKind":{"component":"inline"},"__proto__":{"polluted":true}}},"constructor":{"prototype":{"polluted":true}}}'
128
+ );
129
+
130
+ const merged = mergeFeaturePreferences([base, unsafe]);
131
+
132
+ expect(merged).toEqual({
133
+ artifacts: {
134
+ display: {
135
+ byKind: {
136
+ markdown: "collapsed",
137
+ component: "inline",
138
+ },
139
+ },
140
+ },
141
+ });
142
+ expect(base).toEqual({
143
+ artifacts: {
144
+ display: {
145
+ byKind: { markdown: "collapsed" },
146
+ },
147
+ },
148
+ });
149
+ expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
150
+ });
151
+
152
+ test("allows only curated persisted fields at the runtime boundary", () => {
153
+ const storedJson: unknown = {
154
+ artifacts: {
155
+ enabled: false,
156
+ allowedTypes: [],
157
+ display: {
158
+ default: "sideways",
159
+ files: "inline",
160
+ unknownSelector: "panel",
161
+ },
162
+ filePreview: {
163
+ enabled: false,
164
+ iframeSandbox: "allow-same-origin allow-scripts",
165
+ dangerouslyAllowSameOrigin: true,
166
+ },
167
+ },
168
+ unknownRoot: true,
169
+ };
170
+ const parsed = parseWidgetPreferenceSlice(storedJson);
171
+ expect(parsed.preferences).toEqual({
172
+ artifacts: {
173
+ display: { files: "inline" },
174
+ filePreview: { enabled: false },
175
+ },
176
+ });
177
+ expect(parsed.issues.map((entry) => entry.path)).toEqual(
178
+ expect.arrayContaining([
179
+ "artifacts.enabled",
180
+ "artifacts.allowedTypes",
181
+ "artifacts.display.default",
182
+ "artifacts.display.unknownSelector",
183
+ "artifacts.filePreview.iframeSandbox",
184
+ "artifacts.filePreview.dangerouslyAllowSameOrigin",
185
+ "unknownRoot",
186
+ ])
187
+ );
188
+
189
+ const result = applyFeaturePreferences(
190
+ {
191
+ artifacts: {
192
+ enabled: true,
193
+ allowedTypes: ["markdown"],
194
+ filePreview: {
195
+ iframeSandbox: "allow-scripts",
196
+ dangerouslyAllowSameOrigin: false,
197
+ },
198
+ },
199
+ },
200
+ [storedJson as WidgetPreferenceSlice]
201
+ );
202
+ expect(result.artifacts).toMatchObject({
203
+ enabled: true,
204
+ allowedTypes: ["markdown"],
205
+ display: { files: "inline" },
206
+ filePreview: {
207
+ enabled: false,
208
+ iframeSandbox: "allow-scripts",
209
+ dangerouslyAllowSameOrigin: false,
210
+ },
211
+ });
212
+ });
213
+
214
+ test("preserves base callbacks and the default from a string display config", () => {
215
+ const onArtifactAction = () => true;
216
+ const renderInline = () => document.createElement("div");
217
+ const toolbarAction = {
218
+ id: "export",
219
+ label: "Export",
220
+ onClick: () => undefined,
221
+ };
222
+ const result = applyFeaturePreferences(
223
+ {
224
+ artifacts: {
225
+ display: "collapsed",
226
+ onArtifactAction,
227
+ renderInline,
228
+ toolbarActions: [toolbarAction],
229
+ },
230
+ },
231
+ [{ artifacts: { display: { files: "inline" } } }]
232
+ );
233
+ expect(result.artifacts).toMatchObject({
234
+ display: { default: "collapsed", files: "inline" },
235
+ onArtifactAction,
236
+ renderInline,
237
+ toolbarActions: [toolbarAction],
238
+ });
239
+ });
240
+
241
+ test("rejects invalid media-type selectors but accepts wildcards", () => {
242
+ const parsed = parseWidgetPreferenceSlice({
243
+ artifacts: {
244
+ display: {
245
+ files: {
246
+ byMediaType: {
247
+ "image/*": "inline",
248
+ "*/*": "panel",
249
+ "not a mime": "collapsed",
250
+ },
251
+ },
252
+ },
253
+ },
254
+ });
255
+ expect(parsed.preferences).toEqual({
256
+ artifacts: {
257
+ display: { files: { byMediaType: { "image/*": "inline" } } },
258
+ },
259
+ });
260
+ expect(parsed.issues.map((entry) => entry.path)).toEqual([
261
+ "artifacts.display.files.byMediaType.*/*",
262
+ "artifacts.display.files.byMediaType.not a mime",
263
+ ]);
264
+ });
265
+ });
266
+
267
+ describe("resolveConfigPreferences", () => {
268
+ test("returns the config unchanged when preferences are absent or empty", () => {
269
+ const config: AgentWidgetConfig = {
270
+ apiUrl: "https://api.example.com/chat",
271
+ features: { artifacts: { enabled: true, display: "panel" } },
272
+ };
273
+ expect(resolveConfigPreferences(config)).toBe(config);
274
+ const withEmpty = { ...config, preferences: {} };
275
+ expect(resolveConfigPreferences(withEmpty)).toBe(withEmpty);
276
+ });
277
+
278
+ test("bakes preferences into features and keeps other config keys", () => {
279
+ const resolved = resolveConfigPreferences({
280
+ apiUrl: "https://api.example.com/chat",
281
+ features: {
282
+ artifacts: {
283
+ enabled: true,
284
+ display: { default: "panel", byKind: { component: "collapsed" } },
285
+ },
286
+ },
287
+ preferences: { artifacts: { display: { default: "inline" } } },
288
+ });
289
+ expect(resolved.apiUrl).toBe("https://api.example.com/chat");
290
+ expect(resolved.features?.artifacts?.enabled).toBe(true);
291
+ expect(resolved.features?.artifacts?.display).toEqual({
292
+ default: "inline",
293
+ byKind: { component: "collapsed" },
294
+ });
295
+ });
296
+
297
+ test("a string display preference replaces the base rule subtree", () => {
298
+ const resolved = resolveConfigPreferences({
299
+ features: {
300
+ artifacts: {
301
+ display: { default: "panel", byKind: { markdown: "collapsed" } },
302
+ },
303
+ },
304
+ preferences: { artifacts: { display: "inline" } },
305
+ });
306
+ expect(resolved.features?.artifacts?.display).toBe("inline");
307
+ });
308
+
309
+ test("preferences cannot carry keys outside the allowlist", () => {
310
+ const resolved = resolveConfigPreferences({
311
+ features: {
312
+ artifacts: { enabled: true, filePreview: { iframeSandbox: "allow-scripts" } },
313
+ },
314
+ preferences: {
315
+ artifacts: {
316
+ display: "inline",
317
+ filePreview: { iframeSandbox: "allow-same-origin" },
318
+ },
319
+ } as unknown as WidgetPreferenceSlice,
320
+ });
321
+ expect(resolved.features?.artifacts?.filePreview?.iframeSandbox).toBe(
322
+ "allow-scripts"
323
+ );
324
+ expect(resolved.features?.artifacts?.display).toBe("inline");
325
+ });
326
+ });