@runtypelabs/persona 4.12.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.scroll-additive.test.ts +365 -0
- package/src/ui.scroll.test.ts +19 -6
- package/src/ui.ts +158 -53
- 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/auto-follow.test.ts +38 -0
- package/src/utils/auto-follow.ts +35 -0
- 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
|
@@ -255,4 +255,110 @@ describe("AgentWidgetSession artifacts", () => {
|
|
|
255
255
|
});
|
|
256
256
|
expect(session.getArtifactById(session.getArtifacts()[0].id)?.file).toEqual(file);
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
it("uses exact media-type file rules and the producer preferredMode hint", () => {
|
|
260
|
+
const session = new AgentWidgetSession(
|
|
261
|
+
{
|
|
262
|
+
features: {
|
|
263
|
+
artifacts: {
|
|
264
|
+
enabled: true,
|
|
265
|
+
display: {
|
|
266
|
+
default: "card",
|
|
267
|
+
files: { byMediaType: { "text/html": "inline" } },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
onMessagesChanged: () => {},
|
|
274
|
+
onStatusChanged: () => {},
|
|
275
|
+
onStreamingChanged: () => {},
|
|
276
|
+
onArtifactsState: () => {},
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
const rec = session.upsertArtifact({
|
|
280
|
+
id: "html-app",
|
|
281
|
+
artifactType: "markdown",
|
|
282
|
+
content: "<h1>App</h1>",
|
|
283
|
+
file: { path: "app.html", mimeType: "TEXT/HTML; charset=utf-8" },
|
|
284
|
+
});
|
|
285
|
+
const block = session.getMessages().find((message) => message.id === `artifact-ref-${rec.id}`);
|
|
286
|
+
expect(JSON.parse(block!.rawContent!).component).toBe("PersonaArtifactInline");
|
|
287
|
+
|
|
288
|
+
const hinted = session.upsertArtifact({
|
|
289
|
+
id: "hinted-doc",
|
|
290
|
+
artifactType: "markdown",
|
|
291
|
+
content: "# Live preview",
|
|
292
|
+
presentation: { preferredMode: "inline" },
|
|
293
|
+
});
|
|
294
|
+
const hintedBlock = session
|
|
295
|
+
.getMessages()
|
|
296
|
+
.find((message) => message.id === `artifact-ref-${hinted.id}`);
|
|
297
|
+
expect(JSON.parse(hintedBlock!.rawContent!).component).toBe(
|
|
298
|
+
"PersonaArtifactInline"
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("re-materializes existing artifact blocks after a live display update", () => {
|
|
303
|
+
const onMessagesChanged = vi.fn();
|
|
304
|
+
const session = new AgentWidgetSession(
|
|
305
|
+
{ features: { artifacts: { enabled: true, display: "card" } } },
|
|
306
|
+
{
|
|
307
|
+
onMessagesChanged,
|
|
308
|
+
onStatusChanged: () => {},
|
|
309
|
+
onStreamingChanged: () => {},
|
|
310
|
+
onArtifactsState: () => {},
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
session.upsertArtifact({
|
|
314
|
+
id: "live",
|
|
315
|
+
artifactType: "component",
|
|
316
|
+
component: "Chart",
|
|
317
|
+
props: { values: [1, 2] },
|
|
318
|
+
});
|
|
319
|
+
session.upsertArtifact({
|
|
320
|
+
id: "live-markdown",
|
|
321
|
+
artifactType: "markdown",
|
|
322
|
+
content: "# Report",
|
|
323
|
+
});
|
|
324
|
+
const unmatchedRawContent = JSON.stringify({
|
|
325
|
+
component: "PersonaArtifactCard",
|
|
326
|
+
props: {
|
|
327
|
+
artifactId: "hydrated-without-record",
|
|
328
|
+
artifactType: "markdown",
|
|
329
|
+
status: "complete",
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
session.injectAssistantMessage({
|
|
333
|
+
id: "artifact-ref-hydrated-without-record",
|
|
334
|
+
content: "",
|
|
335
|
+
rawContent: unmatchedRawContent,
|
|
336
|
+
});
|
|
337
|
+
onMessagesChanged.mockClear();
|
|
338
|
+
|
|
339
|
+
session.updateConfig({
|
|
340
|
+
features: { artifacts: { enabled: true, display: "inline" } },
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(onMessagesChanged).toHaveBeenCalledTimes(1);
|
|
344
|
+
const block = session.getMessages().find((message) => message.id === "artifact-ref-live");
|
|
345
|
+
const parsed = JSON.parse(block!.rawContent!);
|
|
346
|
+
expect(parsed.component).toBe("PersonaArtifactInline");
|
|
347
|
+
expect(parsed.props.component).toBe("Chart");
|
|
348
|
+
expect(parsed.props.componentProps).toEqual({ values: [1, 2] });
|
|
349
|
+
const markdownBlock = session
|
|
350
|
+
.getMessages()
|
|
351
|
+
.find((message) => message.id === "artifact-ref-live-markdown");
|
|
352
|
+
const parsedMarkdown = JSON.parse(markdownBlock!.rawContent!);
|
|
353
|
+
expect(parsedMarkdown.component).toBe("PersonaArtifactInline");
|
|
354
|
+
expect(parsedMarkdown.props.markdown).toBe("# Report");
|
|
355
|
+
expect(
|
|
356
|
+
session
|
|
357
|
+
.getMessages()
|
|
358
|
+
.find(
|
|
359
|
+
(message) =>
|
|
360
|
+
message.id === "artifact-ref-hydrated-without-record"
|
|
361
|
+
)?.rawContent
|
|
362
|
+
).toBe(unmatchedRawContent);
|
|
363
|
+
});
|
|
258
364
|
});
|
package/src/client.test.ts
CHANGED
|
@@ -4505,10 +4505,10 @@ describe('AgentWidgetClient - Artifact display modes', () => {
|
|
|
4505
4505
|
});
|
|
4506
4506
|
});
|
|
4507
4507
|
|
|
4508
|
-
it('resolves the mode per artifact
|
|
4508
|
+
it('resolves the mode per artifact kind via display.byKind', async () => {
|
|
4509
4509
|
const display: AgentWidgetArtifactsFeature['display'] = {
|
|
4510
4510
|
default: 'card',
|
|
4511
|
-
|
|
4511
|
+
byKind: { component: 'inline' },
|
|
4512
4512
|
};
|
|
4513
4513
|
|
|
4514
4514
|
const mdEvents = await dispatchAndCollect(markdownArtifactFrames(), {
|
package/src/client.ts
CHANGED
|
@@ -3184,7 +3184,10 @@ export class AgentWidgetClient {
|
|
|
3184
3184
|
artifactIdsWithCards.add(artId);
|
|
3185
3185
|
const displayMode = resolveArtifactDisplayMode(
|
|
3186
3186
|
this.config.features?.artifacts,
|
|
3187
|
-
|
|
3187
|
+
{
|
|
3188
|
+
artifactType: at,
|
|
3189
|
+
...(artFile ? { file: artFile } : {}),
|
|
3190
|
+
}
|
|
3188
3191
|
);
|
|
3189
3192
|
const artComponent =
|
|
3190
3193
|
typeof payload.component === "string" ? payload.component : undefined;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ComponentContext, ComponentRenderer } from "./registry";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PersonaArtifactFileMeta,
|
|
4
|
+
PersonaArtifactPresentation,
|
|
5
|
+
PersonaArtifactRecord
|
|
6
|
+
} from "../types";
|
|
3
7
|
import { fileTypeLabel, basenameOf } from "../utils/artifact-file";
|
|
4
8
|
import {
|
|
5
9
|
applyArtifactStatus,
|
|
@@ -33,6 +37,12 @@ function renderDefaultArtifactCard(
|
|
|
33
37
|
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
34
38
|
const artifactType =
|
|
35
39
|
typeof props.artifactType === "string" ? props.artifactType : "markdown";
|
|
40
|
+
const presentation =
|
|
41
|
+
props.presentation &&
|
|
42
|
+
typeof props.presentation === "object" &&
|
|
43
|
+
!Array.isArray(props.presentation)
|
|
44
|
+
? (props.presentation as PersonaArtifactPresentation)
|
|
45
|
+
: undefined;
|
|
36
46
|
const subtitle = file
|
|
37
47
|
? fileTypeLabel(file)
|
|
38
48
|
: artifactType === "component"
|
|
@@ -83,7 +93,8 @@ function renderDefaultArtifactCard(
|
|
|
83
93
|
title: rawTitle,
|
|
84
94
|
status: "streaming",
|
|
85
95
|
...(typeof props.markdown === "string" ? { markdown: props.markdown } : {}),
|
|
86
|
-
...(file ? { file } : {})
|
|
96
|
+
...(file ? { file } : {}),
|
|
97
|
+
...(presentation ? { presentation } : {})
|
|
87
98
|
};
|
|
88
99
|
const resolved = resolveArtifactStatusLabel(record, artifactsCfg, "card");
|
|
89
100
|
applyArtifactStatus(subtitleEl, resolved, artifactsCfg);
|
|
@@ -2,6 +2,7 @@ import type { ComponentContext, ComponentRenderer } from "./registry";
|
|
|
2
2
|
import type {
|
|
3
3
|
AgentWidgetArtifactsFeature,
|
|
4
4
|
PersonaArtifactFileMeta,
|
|
5
|
+
PersonaArtifactPresentation,
|
|
5
6
|
PersonaArtifactRecord
|
|
6
7
|
} from "../types";
|
|
7
8
|
import { createElement } from "../utils/dom";
|
|
@@ -126,6 +127,12 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
126
127
|
const title =
|
|
127
128
|
typeof props.title === "string" && props.title ? props.title : undefined;
|
|
128
129
|
const status = props.status === "streaming" ? "streaming" : "complete";
|
|
130
|
+
const presentation =
|
|
131
|
+
props.presentation &&
|
|
132
|
+
typeof props.presentation === "object" &&
|
|
133
|
+
!Array.isArray(props.presentation)
|
|
134
|
+
? (props.presentation as PersonaArtifactPresentation)
|
|
135
|
+
: undefined;
|
|
129
136
|
if (props.artifactType === "component") {
|
|
130
137
|
const embedded = props.componentProps;
|
|
131
138
|
const componentProps =
|
|
@@ -138,7 +145,8 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
138
145
|
title,
|
|
139
146
|
status,
|
|
140
147
|
component: typeof props.component === "string" ? props.component : "",
|
|
141
|
-
props: componentProps
|
|
148
|
+
props: componentProps,
|
|
149
|
+
...(presentation ? { presentation } : {})
|
|
142
150
|
};
|
|
143
151
|
}
|
|
144
152
|
const file =
|
|
@@ -151,7 +159,8 @@ function recordFromProps(props: Record<string, unknown>): PersonaArtifactRecord
|
|
|
151
159
|
title,
|
|
152
160
|
status,
|
|
153
161
|
markdown: typeof props.markdown === "string" ? props.markdown : "",
|
|
154
|
-
...(file ? { file } : {})
|
|
162
|
+
...(file ? { file } : {}),
|
|
163
|
+
...(presentation ? { presentation } : {})
|
|
155
164
|
};
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -17,6 +17,8 @@ describe("buildComposer (full column-stacked composer)", () => {
|
|
|
17
17
|
expect(elements.composerForm.tagName).toBe("FORM");
|
|
18
18
|
expect(elements.composerForm.getAttribute("data-persona-composer-form")).toBe("");
|
|
19
19
|
expect(elements.composerForm.classList.contains("persona-flex-col")).toBe(true);
|
|
20
|
+
expect(elements.composerForm.classList.contains("persona-border-persona-border")).toBe(true);
|
|
21
|
+
expect(elements.composerForm.classList.contains("persona-border-gray-200")).toBe(false);
|
|
20
22
|
|
|
21
23
|
expect(elements.textarea.getAttribute("data-persona-composer-input")).toBe("");
|
|
22
24
|
expect(elements.sendButton.getAttribute("data-persona-composer-submit")).toBe("");
|
|
@@ -67,7 +67,7 @@ export const buildComposer = (context: ComposerBuildContext): ComposerElements =
|
|
|
67
67
|
|
|
68
68
|
const composerForm = createNode("form", {
|
|
69
69
|
className:
|
|
70
|
-
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-
|
|
70
|
+
"persona-widget-composer persona-flex persona-flex-col persona-gap-2 persona-rounded-2xl persona-border persona-border-persona-border persona-bg-persona-input-background persona-px-4 persona-py-3",
|
|
71
71
|
attrs: { "data-persona-composer-form": "" },
|
|
72
72
|
style: { outline: "none" },
|
|
73
73
|
});
|
|
@@ -19,6 +19,8 @@ describe("createComposerTextarea", () => {
|
|
|
19
19
|
expect(textarea.tagName).toBe("TEXTAREA");
|
|
20
20
|
expect(textarea.getAttribute("data-persona-composer-input")).toBe("");
|
|
21
21
|
expect(textarea.classList.contains("persona-composer-textarea")).toBe(true);
|
|
22
|
+
expect(textarea.classList.contains("persona-text-persona-text")).toBe(true);
|
|
23
|
+
expect(textarea.classList.contains("persona-text-persona-primary")).toBe(false);
|
|
22
24
|
});
|
|
23
25
|
|
|
24
26
|
it("attachAutoResize wires an input listener that grows up to maxHeight", () => {
|
|
@@ -32,7 +32,7 @@ export const createComposerTextarea = (config?: AgentWidgetConfig): ComposerText
|
|
|
32
32
|
textarea.setAttribute("data-persona-composer-input", "");
|
|
33
33
|
textarea.placeholder = config?.copy?.inputPlaceholder ?? "Type your message…";
|
|
34
34
|
textarea.className =
|
|
35
|
-
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-
|
|
35
|
+
"persona-w-full persona-min-h-[24px] persona-resize-none persona-border-none persona-bg-transparent persona-text-sm persona-text-persona-text focus:persona-outline-none focus:persona-border-none persona-composer-textarea";
|
|
36
36
|
textarea.rows = 1;
|
|
37
37
|
|
|
38
38
|
textarea.style.fontFamily =
|
|
@@ -276,7 +276,7 @@ describe("createStandardBubble", () => {
|
|
|
276
276
|
expect(userBubble.classList.contains("persona-bg-persona-accent")).toBe(true);
|
|
277
277
|
expect(userBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
278
278
|
expect(userBubble.classList.contains("persona-px-3")).toBe(true);
|
|
279
|
-
expect(assistantBubble.classList.contains("persona-bg-persona-
|
|
279
|
+
expect(assistantBubble.classList.contains("persona-bg-persona-container")).toBe(
|
|
280
280
|
true
|
|
281
281
|
);
|
|
282
282
|
expect(assistantBubble.classList.contains("persona-rounded-lg")).toBe(true);
|
|
@@ -302,6 +302,12 @@ describe("createStandardBubble", () => {
|
|
|
302
302
|
|
|
303
303
|
expect(userMessage.style.backgroundColor).toBe("transparent");
|
|
304
304
|
expect(assistantMessage.style.backgroundColor).toBe("transparent");
|
|
305
|
+
expect(assistantMessage.style.color).toBe(
|
|
306
|
+
"var(--persona-message-assistant-text, var(--persona-text))"
|
|
307
|
+
);
|
|
308
|
+
expect(assistantMessage.classList.contains("persona-text-persona-text")).toBe(
|
|
309
|
+
true
|
|
310
|
+
);
|
|
305
311
|
expect(userMessage.classList.contains("persona-px-3")).toBe(false);
|
|
306
312
|
expect(assistantMessage.classList.contains("persona-px-3")).toBe(false);
|
|
307
313
|
});
|
|
@@ -649,7 +649,7 @@ const createTimestamp = (
|
|
|
649
649
|
/**
|
|
650
650
|
* Get bubble classes based on layout preset
|
|
651
651
|
*/
|
|
652
|
-
const getBubbleClasses = (
|
|
652
|
+
export const getBubbleClasses = (
|
|
653
653
|
role: "user" | "assistant" | "system",
|
|
654
654
|
layout: AgentWidgetMessageLayoutConfig["layout"] = "bubble"
|
|
655
655
|
): string[] => {
|
|
@@ -668,7 +668,7 @@ const getBubbleClasses = (
|
|
|
668
668
|
} else {
|
|
669
669
|
baseClasses.push(
|
|
670
670
|
"persona-message-assistant-bubble",
|
|
671
|
-
"persona-text-persona-
|
|
671
|
+
"persona-text-persona-text",
|
|
672
672
|
"persona-py-2"
|
|
673
673
|
);
|
|
674
674
|
}
|
|
@@ -693,8 +693,8 @@ const getBubbleClasses = (
|
|
|
693
693
|
} else {
|
|
694
694
|
baseClasses.push(
|
|
695
695
|
"persona-message-assistant-bubble",
|
|
696
|
-
"persona-bg-persona-
|
|
697
|
-
"persona-text-persona-
|
|
696
|
+
"persona-bg-persona-container",
|
|
697
|
+
"persona-text-persona-text",
|
|
698
698
|
"persona-px-3",
|
|
699
699
|
"persona-py-2",
|
|
700
700
|
"persona-rounded-lg"
|
|
@@ -723,10 +723,10 @@ const getBubbleClasses = (
|
|
|
723
723
|
} else {
|
|
724
724
|
baseClasses.push(
|
|
725
725
|
"persona-message-assistant-bubble",
|
|
726
|
-
"persona-bg-persona-
|
|
726
|
+
"persona-bg-persona-container",
|
|
727
727
|
"persona-border",
|
|
728
728
|
"persona-border-persona-message-border",
|
|
729
|
-
"persona-text-persona-
|
|
729
|
+
"persona-text-persona-text",
|
|
730
730
|
"persona-px-5",
|
|
731
731
|
"persona-py-3"
|
|
732
732
|
);
|
|
@@ -879,12 +879,15 @@ export const createStandardBubble = (
|
|
|
879
879
|
// Apply component-level color overrides via CSS variables
|
|
880
880
|
if (layout === "flat") {
|
|
881
881
|
bubble.style.backgroundColor = "transparent";
|
|
882
|
-
bubble.style.color =
|
|
882
|
+
bubble.style.color =
|
|
883
|
+
message.role === "user"
|
|
884
|
+
? "var(--persona-primary, var(--persona-text))"
|
|
885
|
+
: "var(--persona-message-assistant-text, var(--persona-text))";
|
|
883
886
|
} else if (message.role === "user") {
|
|
884
887
|
bubble.style.backgroundColor = 'var(--persona-message-user-bg, var(--persona-accent))';
|
|
885
888
|
bubble.style.color = 'var(--persona-message-user-text, white)';
|
|
886
889
|
} else if (message.role === "assistant") {
|
|
887
|
-
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-
|
|
890
|
+
bubble.style.backgroundColor = 'var(--persona-message-assistant-bg, var(--persona-container))';
|
|
888
891
|
bubble.style.color = 'var(--persona-message-assistant-text, var(--persona-text))';
|
|
889
892
|
}
|
|
890
893
|
|
package/src/index-core.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
|
|
6
6
|
export type {
|
|
7
7
|
AgentWidgetConfig,
|
|
8
|
+
AgentWidgetStorageAdapter,
|
|
9
|
+
AgentWidgetStoredState,
|
|
8
10
|
AgentWidgetConfigPatch,
|
|
9
11
|
TargetResolver,
|
|
10
12
|
ResolvedTarget,
|
|
@@ -12,12 +14,19 @@ export type {
|
|
|
12
14
|
AgentWidgetArtifactsFeature,
|
|
13
15
|
AgentWidgetArtifactsLayoutConfig,
|
|
14
16
|
PersonaArtifactDisplayMode,
|
|
17
|
+
PersonaArtifactDisplayModeInput,
|
|
18
|
+
PersonaArtifactDisplayRules,
|
|
19
|
+
PersonaArtifactFilesDisplayRules,
|
|
15
20
|
PersonaArtifactKind,
|
|
21
|
+
PersonaArtifactPresentation,
|
|
16
22
|
PersonaArtifactRecord,
|
|
17
23
|
PersonaArtifactActionContext,
|
|
18
24
|
PersonaArtifactStatusLabelContext,
|
|
19
25
|
PersonaArtifactCustomAction,
|
|
20
26
|
PersonaArtifactManualUpsert,
|
|
27
|
+
WidgetPreferenceCapabilities,
|
|
28
|
+
WidgetArtifactLayoutPreference,
|
|
29
|
+
WidgetPreferenceSlice,
|
|
21
30
|
ArtifactConfigPayload,
|
|
22
31
|
AgentWidgetInitOptions,
|
|
23
32
|
AgentWidgetMessage,
|
|
@@ -134,6 +143,31 @@ export type {
|
|
|
134
143
|
AgentWidgetAskUserQuestionStyles
|
|
135
144
|
} from "./types";
|
|
136
145
|
|
|
146
|
+
export {
|
|
147
|
+
applyFeaturePreferences,
|
|
148
|
+
parseWidgetPreferenceSlice,
|
|
149
|
+
resolveConfigPreferences,
|
|
150
|
+
normalizeMediaType,
|
|
151
|
+
} from "./utils/feature-preferences";
|
|
152
|
+
|
|
153
|
+
export type {
|
|
154
|
+
WidgetPreferenceParseIssueCode,
|
|
155
|
+
WidgetPreferenceParseIssue,
|
|
156
|
+
WidgetPreferenceParseResult,
|
|
157
|
+
} from "./utils/feature-preferences";
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
canonicalArtifactDisplayMode,
|
|
161
|
+
resolveArtifactDisplay,
|
|
162
|
+
resolveArtifactDisplayMode,
|
|
163
|
+
} from "./utils/artifact-display";
|
|
164
|
+
|
|
165
|
+
export type {
|
|
166
|
+
PersonaArtifactDisplayDescriptor,
|
|
167
|
+
PersonaArtifactDisplayMatch,
|
|
168
|
+
PersonaArtifactDisplayResolution,
|
|
169
|
+
} from "./utils/artifact-display";
|
|
170
|
+
|
|
137
171
|
export type {
|
|
138
172
|
RuntypeExecutionStreamEvent,
|
|
139
173
|
RuntypeStreamEventOf,
|
package/src/session.ts
CHANGED
|
@@ -858,7 +858,11 @@ export class AgentWidgetSession {
|
|
|
858
858
|
}
|
|
859
859
|
|
|
860
860
|
public updateConfig(next: AgentWidgetConfig) {
|
|
861
|
+
const previousArtifactDisplay = this.config.features?.artifacts?.display;
|
|
861
862
|
const merged = { ...this.config, ...next };
|
|
863
|
+
const artifactDisplayChanged =
|
|
864
|
+
JSON.stringify(previousArtifactDisplay) !==
|
|
865
|
+
JSON.stringify(merged.features?.artifacts?.display);
|
|
862
866
|
|
|
863
867
|
// Connection/request-shaping change (apiUrl, clientToken, webmcp, headers,
|
|
864
868
|
// parser, agent, …) → full client rebuild. UI-only change (theme, copy,
|
|
@@ -870,6 +874,7 @@ export class AgentWidgetSession {
|
|
|
870
874
|
if (!connectionConfigChanged(this.config, merged)) {
|
|
871
875
|
this.config = merged;
|
|
872
876
|
this.client.updateConfig(merged);
|
|
877
|
+
if (artifactDisplayChanged) this.refreshArtifactReferenceBlocks();
|
|
873
878
|
return;
|
|
874
879
|
}
|
|
875
880
|
|
|
@@ -886,6 +891,7 @@ export class AgentWidgetSession {
|
|
|
886
891
|
const prevSSECallback = this.client.getSSEEventCallback();
|
|
887
892
|
this.config = merged;
|
|
888
893
|
this.client = new AgentWidgetClient(this.config);
|
|
894
|
+
if (artifactDisplayChanged) this.refreshArtifactReferenceBlocks();
|
|
889
895
|
this.wireDefaultWebMcpConfirm();
|
|
890
896
|
if (prevSSECallback) {
|
|
891
897
|
this.client.setSSEEventCallback(prevSSECallback);
|
|
@@ -2735,7 +2741,8 @@ export class AgentWidgetSession {
|
|
|
2735
2741
|
title: manual.title,
|
|
2736
2742
|
status: "complete",
|
|
2737
2743
|
markdown: manual.content,
|
|
2738
|
-
...(manual.file ? { file: manual.file } : {})
|
|
2744
|
+
...(manual.file ? { file: manual.file } : {}),
|
|
2745
|
+
...(manual.presentation ? { presentation: manual.presentation } : {})
|
|
2739
2746
|
}
|
|
2740
2747
|
: {
|
|
2741
2748
|
id,
|
|
@@ -2743,7 +2750,8 @@ export class AgentWidgetSession {
|
|
|
2743
2750
|
title: manual.title,
|
|
2744
2751
|
status: "complete",
|
|
2745
2752
|
component: manual.component,
|
|
2746
|
-
props: manual.props ?? {}
|
|
2753
|
+
props: manual.props ?? {},
|
|
2754
|
+
...(manual.presentation ? { presentation: manual.presentation } : {})
|
|
2747
2755
|
};
|
|
2748
2756
|
this.artifacts.set(id, rec);
|
|
2749
2757
|
this.selectedArtifactId = id;
|
|
@@ -2757,7 +2765,7 @@ export class AgentWidgetSession {
|
|
|
2757
2765
|
/**
|
|
2758
2766
|
* Injects (or refreshes) the in-thread artifact block for a programmatically
|
|
2759
2767
|
* upserted artifact, matching the streamed UX: the resolved display mode
|
|
2760
|
-
* picks the component ("
|
|
2768
|
+
* picks the component ("collapsed"/"panel" → reference card, "inline" → inline
|
|
2761
2769
|
* preview).
|
|
2762
2770
|
*
|
|
2763
2771
|
* Unlike the streamed path (which embeds content on `artifact_complete`),
|
|
@@ -2774,7 +2782,7 @@ export class AgentWidgetSession {
|
|
|
2774
2782
|
const refId = `artifact-ref-${rec.id}`;
|
|
2775
2783
|
const displayMode = resolveArtifactDisplayMode(
|
|
2776
2784
|
this.config.features?.artifacts,
|
|
2777
|
-
rec
|
|
2785
|
+
rec
|
|
2778
2786
|
);
|
|
2779
2787
|
const rawContent = buildArtifactRefRawContent(displayMode, {
|
|
2780
2788
|
artifactId: rec.id,
|
|
@@ -2782,6 +2790,7 @@ export class AgentWidgetSession {
|
|
|
2782
2790
|
artifactType: rec.artifactType,
|
|
2783
2791
|
status: "complete",
|
|
2784
2792
|
...(rec.file ? { file: rec.file } : {}),
|
|
2793
|
+
...(rec.presentation ? { presentation: rec.presentation } : {}),
|
|
2785
2794
|
...(rec.component ? { component: rec.component } : {}),
|
|
2786
2795
|
...(rec.props ? { componentProps: rec.props } : {}),
|
|
2787
2796
|
...(rec.markdown !== undefined ? { markdown: rec.markdown } : {})
|
|
@@ -2803,6 +2812,38 @@ export class AgentWidgetSession {
|
|
|
2803
2812
|
});
|
|
2804
2813
|
}
|
|
2805
2814
|
|
|
2815
|
+
/**
|
|
2816
|
+
* Re-materialize existing transcript artifact blocks after a live display
|
|
2817
|
+
* preference update. One messages callback keeps conversion atomic.
|
|
2818
|
+
*/
|
|
2819
|
+
private refreshArtifactReferenceBlocks(): void {
|
|
2820
|
+
let changed = false;
|
|
2821
|
+
for (const rec of this.artifacts.values()) {
|
|
2822
|
+
const message = this.messages.find((candidate) => candidate.id === `artifact-ref-${rec.id}`);
|
|
2823
|
+
if (!message) continue;
|
|
2824
|
+
const displayMode = resolveArtifactDisplayMode(
|
|
2825
|
+
this.config.features?.artifacts,
|
|
2826
|
+
rec
|
|
2827
|
+
);
|
|
2828
|
+
const rawContent = buildArtifactRefRawContent(displayMode, {
|
|
2829
|
+
artifactId: rec.id,
|
|
2830
|
+
title: rec.title,
|
|
2831
|
+
artifactType: rec.artifactType,
|
|
2832
|
+
status: rec.status,
|
|
2833
|
+
...(rec.file ? { file: rec.file } : {}),
|
|
2834
|
+
...(rec.presentation ? { presentation: rec.presentation } : {}),
|
|
2835
|
+
...(rec.component ? { component: rec.component } : {}),
|
|
2836
|
+
...(rec.props ? { componentProps: rec.props } : {}),
|
|
2837
|
+
...(rec.markdown !== undefined ? { markdown: rec.markdown } : {}),
|
|
2838
|
+
});
|
|
2839
|
+
if (message.rawContent === rawContent) continue;
|
|
2840
|
+
message.rawContent = rawContent;
|
|
2841
|
+
message.streaming = rec.status === "streaming";
|
|
2842
|
+
changed = true;
|
|
2843
|
+
}
|
|
2844
|
+
if (changed) this.callbacks.onMessagesChanged([...this.messages]);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2806
2847
|
private clearArtifactState(): void {
|
|
2807
2848
|
if (this.artifacts.size === 0 && this.selectedArtifactId === null) return;
|
|
2808
2849
|
this.artifacts.clear();
|
package/src/styles/widget.css
CHANGED
|
@@ -294,6 +294,10 @@
|
|
|
294
294
|
color: var(--persona-primary, #171717);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
.persona-text-persona-text {
|
|
298
|
+
color: var(--persona-text, #111827);
|
|
299
|
+
}
|
|
300
|
+
|
|
297
301
|
.persona-text-persona-call-to-action {
|
|
298
302
|
color: var(--persona-call-to-action, var(--persona-text-inverse, #ffffff));
|
|
299
303
|
}
|
|
@@ -930,6 +934,11 @@
|
|
|
930
934
|
box-shadow: none !important;
|
|
931
935
|
}
|
|
932
936
|
|
|
937
|
+
.persona-composer-textarea::placeholder {
|
|
938
|
+
color: var(--persona-input-placeholder, var(--persona-text-muted, #9ca3af));
|
|
939
|
+
opacity: 1;
|
|
940
|
+
}
|
|
941
|
+
|
|
933
942
|
/* Prevent iOS Safari from zooming on input focus (requires 16px minimum) */
|
|
934
943
|
@media (hover: none) and (pointer: coarse) {
|
|
935
944
|
.persona-composer-textarea {
|
|
@@ -4928,7 +4937,7 @@
|
|
|
4928
4937
|
|
|
4929
4938
|
.persona-composer-contenteditable:empty::before {
|
|
4930
4939
|
content: attr(data-placeholder);
|
|
4931
|
-
color: var(--persona-placeholder, var(--persona-muted, #9ca3af));
|
|
4940
|
+
color: var(--persona-input-placeholder, var(--persona-text-muted, #9ca3af));
|
|
4932
4941
|
pointer-events: none;
|
|
4933
4942
|
}
|
|
4934
4943
|
|
|
@@ -43,6 +43,8 @@ export {
|
|
|
43
43
|
COMPONENT_COLOR_SECTIONS,
|
|
44
44
|
CONFIGURE_SECTIONS,
|
|
45
45
|
CONFIGURE_SUB_GROUPS,
|
|
46
|
+
STREAM_AND_ARTIFACTS_SUB_GROUP,
|
|
47
|
+
ARTIFACT_DISPLAY_PREFERENCE_SECTION,
|
|
46
48
|
ALL_TABS,
|
|
47
49
|
scopeSection,
|
|
48
50
|
findSection,
|
|
@@ -90,6 +92,9 @@ export type {
|
|
|
90
92
|
TranscriptStreamFrame,
|
|
91
93
|
BuildTranscriptStreamFramesOptions,
|
|
92
94
|
} from './preview-utils';
|
|
95
|
+
// Type-only: the full preview implementation stays in
|
|
96
|
+
// `@runtypelabs/persona/theme-editor/preview`.
|
|
97
|
+
export type { CompareMode, PreviewShellMode } from './preview';
|
|
93
98
|
|
|
94
99
|
// Role mappings (Interface Roles editor)
|
|
95
100
|
export {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { AgentWidgetConfig } from '../types';
|
|
3
|
+
import { getActiveTheme, themeToCssVariables } from '../utils/theme';
|
|
4
|
+
import { BUILT_IN_PRESETS, getThemeEditorPreset } from './presets';
|
|
5
|
+
|
|
6
|
+
describe('theme editor presets', () => {
|
|
7
|
+
it('offers the paired Persona default before the single-scheme presets', () => {
|
|
8
|
+
const preset = getThemeEditorPreset('persona-default');
|
|
9
|
+
|
|
10
|
+
expect(BUILT_IN_PRESETS[0]).toBe(preset);
|
|
11
|
+
expect(preset).toMatchObject({
|
|
12
|
+
name: 'Persona Default',
|
|
13
|
+
tags: ['light', 'dark', 'adaptive'],
|
|
14
|
+
preview: { surface: '#ffffff' },
|
|
15
|
+
darkPreview: { surface: '#1f2937' },
|
|
16
|
+
});
|
|
17
|
+
expect(preset?.darkTheme).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('resolves the paired default to distinct light and dark surfaces', () => {
|
|
21
|
+
const preset = getThemeEditorPreset('persona-default')!;
|
|
22
|
+
const config = {
|
|
23
|
+
theme: preset.theme,
|
|
24
|
+
darkTheme: preset.darkTheme,
|
|
25
|
+
} as AgentWidgetConfig;
|
|
26
|
+
|
|
27
|
+
const light = themeToCssVariables(getActiveTheme({ ...config, colorScheme: 'light' }));
|
|
28
|
+
const dark = themeToCssVariables(getActiveTheme({ ...config, colorScheme: 'dark' }));
|
|
29
|
+
|
|
30
|
+
expect(light).toMatchObject({
|
|
31
|
+
'--persona-surface': '#f9fafb',
|
|
32
|
+
'--persona-background': '#f9fafb',
|
|
33
|
+
'--persona-text': '#111827',
|
|
34
|
+
'--persona-border': '#e5e7eb',
|
|
35
|
+
});
|
|
36
|
+
expect(dark).toMatchObject({
|
|
37
|
+
'--persona-surface': '#1f2937',
|
|
38
|
+
'--persona-background': '#111827',
|
|
39
|
+
'--persona-text': '#f3f4f6',
|
|
40
|
+
'--persona-border': '#374151',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|