@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.
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +98 -6
- package/dist/context-mentions.d.ts +98 -6
- package/dist/index.cjs +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -8
- package/dist/index.d.ts +183 -8
- package/dist/index.global.js +54 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -67
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +98 -6
- package/dist/smart-dom-reader.d.ts +98 -6
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +100 -6
- package/dist/theme-editor-preview.d.ts +100 -6
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +134 -7
- package/dist/theme-editor.d.ts +134 -7
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +2 -0
- package/src/components/composer-parts.ts +1 -1
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/index-core.ts +34 -0
- package/src/session.ts +45 -4
- package/src/styles/widget.css +10 -1
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- package/src/theme-editor/sections.test.ts +39 -7
- package/src/theme-editor/sections.ts +60 -8
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/types.ts +139 -9
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.ts +43 -25
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- package/src/utils/config-merge.test.ts +60 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/theme.test.ts +92 -0
- package/src/utils/tokens.ts +7 -7
|
@@ -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
|
+
}
|
package/src/utils/theme.test.ts
CHANGED
|
@@ -137,6 +137,98 @@ describe('theme utils', () => {
|
|
|
137
137
|
expect(cssVars['--persona-button-radius']).toBe('6px');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it.each([
|
|
141
|
+
{
|
|
142
|
+
role: 'surface',
|
|
143
|
+
sentinel: '#010203',
|
|
144
|
+
consumers: [
|
|
145
|
+
'--persona-components-input-background',
|
|
146
|
+
'--persona-input-background',
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
role: 'container',
|
|
151
|
+
sentinel: '#0a0b0c',
|
|
152
|
+
consumers: [
|
|
153
|
+
'--persona-components-message-assistant-background',
|
|
154
|
+
'--persona-message-assistant-bg',
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
role: 'textMuted',
|
|
159
|
+
sentinel: '#0d0e0f',
|
|
160
|
+
consumers: [
|
|
161
|
+
'--persona-components-input-placeholder',
|
|
162
|
+
'--persona-input-placeholder',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
role: 'text',
|
|
167
|
+
sentinel: '#121314',
|
|
168
|
+
consumers: [
|
|
169
|
+
'--persona-components-message-assistant-text',
|
|
170
|
+
'--persona-message-assistant-text',
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
role: 'border',
|
|
175
|
+
sentinel: '#232425',
|
|
176
|
+
consumers: [
|
|
177
|
+
'--persona-components-message-assistant-border',
|
|
178
|
+
'--persona-message-assistant-border',
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
])(
|
|
182
|
+
'cascades semantic.colors.$role to its default input and assistant consumers',
|
|
183
|
+
({ role, sentinel, consumers }) => {
|
|
184
|
+
const theme = createTheme({
|
|
185
|
+
semantic: {
|
|
186
|
+
colors: {
|
|
187
|
+
[role]: sentinel,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
} as any);
|
|
191
|
+
const cssVars = themeToCssVariables(theme);
|
|
192
|
+
|
|
193
|
+
for (const consumer of consumers) {
|
|
194
|
+
expect(cssVars[consumer]).toBe(sentinel);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
it('keeps explicit input and assistant component colors above semantic defaults', () => {
|
|
200
|
+
const theme = createTheme({
|
|
201
|
+
semantic: {
|
|
202
|
+
colors: {
|
|
203
|
+
surface: '#010203',
|
|
204
|
+
container: '#0a0b0c',
|
|
205
|
+
text: '#121314',
|
|
206
|
+
border: '#232425',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
components: {
|
|
210
|
+
input: {
|
|
211
|
+
background: '#343536',
|
|
212
|
+
placeholder: '#3a3b3c',
|
|
213
|
+
},
|
|
214
|
+
message: {
|
|
215
|
+
assistant: {
|
|
216
|
+
background: '#454647',
|
|
217
|
+
text: '#565758',
|
|
218
|
+
border: '#676869',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
} as any);
|
|
223
|
+
const cssVars = themeToCssVariables(theme);
|
|
224
|
+
|
|
225
|
+
expect(cssVars['--persona-input-background']).toBe('#343536');
|
|
226
|
+
expect(cssVars['--persona-input-placeholder']).toBe('#3a3b3c');
|
|
227
|
+
expect(cssVars['--persona-message-assistant-bg']).toBe('#454647');
|
|
228
|
+
expect(cssVars['--persona-message-assistant-text']).toBe('#565758');
|
|
229
|
+
expect(cssVars['--persona-message-assistant-border']).toBe('#676869');
|
|
230
|
+
});
|
|
231
|
+
|
|
140
232
|
it('maps markdown link and optional heading tokens to consumer CSS vars', () => {
|
|
141
233
|
const theme = createTheme({
|
|
142
234
|
components: {
|
package/src/utils/tokens.ts
CHANGED
|
@@ -269,9 +269,9 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
269
269
|
},
|
|
270
270
|
},
|
|
271
271
|
input: {
|
|
272
|
-
// Input role:
|
|
273
|
-
background: '
|
|
274
|
-
placeholder: '
|
|
272
|
+
// Input role: semantic surface so host light/dark schemes cascade.
|
|
273
|
+
background: 'semantic.colors.surface',
|
|
274
|
+
placeholder: 'semantic.colors.textMuted',
|
|
275
275
|
borderRadius: 'palette.radius.lg',
|
|
276
276
|
padding: 'semantic.spacing.md',
|
|
277
277
|
focus: {
|
|
@@ -319,11 +319,11 @@ export const DEFAULT_COMPONENTS: ComponentTokens = {
|
|
|
319
319
|
shadow: 'palette.shadows.sm',
|
|
320
320
|
},
|
|
321
321
|
assistant: {
|
|
322
|
-
// Assistant Messages role:
|
|
323
|
-
background: '
|
|
324
|
-
text: '
|
|
322
|
+
// Assistant Messages role: semantic neutral container.
|
|
323
|
+
background: 'semantic.colors.container',
|
|
324
|
+
text: 'semantic.colors.text',
|
|
325
325
|
borderRadius: 'palette.radius.lg',
|
|
326
|
-
border: '
|
|
326
|
+
border: 'semantic.colors.border',
|
|
327
327
|
shadow: 'palette.shadows.sm',
|
|
328
328
|
},
|
|
329
329
|
border: 'semantic.colors.border',
|