@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.
- package/README.md +4 -4
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
- package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-QJ4LV2KW.js +1 -0
- package/dist/codegen.cjs +12 -11
- package/dist/codegen.js +14 -13
- package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
- package/dist/context-mentions.cjs +11 -8
- package/dist/context-mentions.d.cts +339 -22
- package/dist/context-mentions.d.ts +339 -22
- package/dist/context-mentions.js +11 -8
- package/dist/index.cjs +68 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +483 -48
- package/dist/index.d.ts +483 -48
- package/dist/index.global.js +61 -58
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +5 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +339 -22
- package/dist/smart-dom-reader.d.ts +339 -22
- package/dist/theme-editor-preview.cjs +67 -67
- package/dist/theme-editor-preview.d.cts +355 -23
- package/dist/theme-editor-preview.d.ts +355 -23
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +376 -24
- package/dist/theme-editor.d.ts +376 -24
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +38 -0
- package/dist/theme-reference.d.ts +38 -0
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/ask-user-question-tool.ts +10 -7
- 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/artifact-preview.ts +1 -1
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +105 -4
- package/src/components/composer-parts.ts +54 -16
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/components/messages.ts +5 -2
- package/src/components/panel.ts +31 -5
- package/src/components/pill-composer-builder.test.ts +18 -0
- package/src/components/suggestions.test.ts +233 -0
- package/src/components/suggestions.ts +463 -81
- package/src/components/widget-view.ts +4 -0
- package/src/defaults.ts +5 -3
- package/src/generated/runtype-openapi-contract.ts +17 -0
- package/src/index-core.ts +52 -2
- package/src/launcher-global.ts +1 -1
- package/src/plugins/types.ts +71 -3
- package/src/presets.ts +3 -3
- package/src/session.ts +55 -12
- package/src/styles/widget-styles.test.ts +162 -0
- package/src/styles/widget.css +440 -1
- package/src/suggest-replies-tool.test.ts +233 -2
- package/src/suggest-replies-tool.ts +115 -39
- 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/preview-utils.test.ts +34 -0
- package/src/theme-editor/preview-utils.ts +77 -2
- package/src/theme-editor/sections.test.ts +72 -7
- package/src/theme-editor/sections.ts +113 -13
- 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/theme-reference.ts +9 -2
- package/src/types/theme.ts +32 -0
- package/src/types.ts +291 -25
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.detached-panel.test.ts +1 -1
- package/src/ui.suggest-replies.test.ts +1066 -1
- package/src/ui.ts +311 -45
- 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/artifact-file.test.ts +1 -1
- package/src/utils/code-generators.test.ts +40 -0
- package/src/utils/code-generators.ts +22 -0
- package/src/utils/config-merge.test.ts +84 -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/icons.test.ts +26 -0
- package/src/utils/icons.ts +8 -0
- package/src/utils/theme.test.ts +171 -0
- package/src/utils/tokens.ts +71 -7
- package/dist/chunk-MMUPR2JW.js +0 -1
|
@@ -1,42 +1,206 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
resolveArtifactDisplay,
|
|
4
|
+
resolveArtifactDisplayMode,
|
|
5
|
+
} from "./artifact-display";
|
|
3
6
|
|
|
4
7
|
describe("resolveArtifactDisplayMode", () => {
|
|
8
|
+
const markdown = { artifactType: "markdown" as const };
|
|
9
|
+
const component = { artifactType: "component" as const };
|
|
10
|
+
|
|
5
11
|
it("returns 'panel' when the feature is undefined", () => {
|
|
6
|
-
expect(resolveArtifactDisplayMode(undefined,
|
|
12
|
+
expect(resolveArtifactDisplayMode(undefined, markdown)).toBe("panel");
|
|
7
13
|
});
|
|
8
14
|
|
|
9
15
|
it("returns 'panel' when display is unset", () => {
|
|
10
|
-
expect(resolveArtifactDisplayMode({ enabled: true },
|
|
16
|
+
expect(resolveArtifactDisplayMode({ enabled: true }, markdown)).toBe("panel");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("canonicalizes the deprecated 'card' alias to 'collapsed'", () => {
|
|
20
|
+
expect(resolveArtifactDisplayMode({ display: "card" }, markdown)).toBe(
|
|
21
|
+
"collapsed"
|
|
22
|
+
);
|
|
23
|
+
expect(
|
|
24
|
+
resolveArtifactDisplayMode(
|
|
25
|
+
{ display: { files: "card" } },
|
|
26
|
+
{
|
|
27
|
+
artifactType: "markdown",
|
|
28
|
+
file: { path: "a.txt", mimeType: "text/plain" },
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
).toBe("collapsed");
|
|
32
|
+
expect(
|
|
33
|
+
resolveArtifactDisplayMode(
|
|
34
|
+
{ display: { byKind: { component: "card" } } },
|
|
35
|
+
component
|
|
36
|
+
)
|
|
37
|
+
).toBe("collapsed");
|
|
11
38
|
});
|
|
12
39
|
|
|
13
40
|
it("returns the string form directly", () => {
|
|
14
|
-
expect(resolveArtifactDisplayMode({ display: "inline" },
|
|
15
|
-
expect(resolveArtifactDisplayMode({ display: "
|
|
41
|
+
expect(resolveArtifactDisplayMode({ display: "inline" }, markdown)).toBe("inline");
|
|
42
|
+
expect(resolveArtifactDisplayMode({ display: "collapsed" }, component)).toBe("collapsed");
|
|
16
43
|
});
|
|
17
44
|
|
|
18
|
-
it("uses the object default when no
|
|
19
|
-
expect(resolveArtifactDisplayMode({ display: { default: "
|
|
45
|
+
it("uses the object default when no byKind entry matches", () => {
|
|
46
|
+
expect(resolveArtifactDisplayMode({ display: { default: "collapsed" } }, markdown)).toBe("collapsed");
|
|
20
47
|
});
|
|
21
48
|
|
|
22
|
-
it("prefers a
|
|
49
|
+
it("prefers a byKind override over the default", () => {
|
|
23
50
|
const feature = {
|
|
24
|
-
display: { default: "panel" as const,
|
|
51
|
+
display: { default: "panel" as const, byKind: { markdown: "inline" as const } }
|
|
25
52
|
};
|
|
26
|
-
expect(resolveArtifactDisplayMode(feature,
|
|
53
|
+
expect(resolveArtifactDisplayMode(feature, markdown)).toBe("inline");
|
|
27
54
|
});
|
|
28
55
|
|
|
29
|
-
it("falls back to the default on a
|
|
56
|
+
it("falls back to the default on a byKind miss", () => {
|
|
30
57
|
const feature = {
|
|
31
|
-
display: { default: "
|
|
58
|
+
display: { default: "collapsed" as const, byKind: { markdown: "inline" as const } }
|
|
32
59
|
};
|
|
33
|
-
expect(resolveArtifactDisplayMode(feature,
|
|
60
|
+
expect(resolveArtifactDisplayMode(feature, component)).toBe("collapsed");
|
|
34
61
|
});
|
|
35
62
|
|
|
36
|
-
it("returns 'panel' for an object with neither default nor a matching
|
|
37
|
-
expect(resolveArtifactDisplayMode({ display: {} },
|
|
63
|
+
it("returns 'panel' for an object with neither default nor a matching byKind", () => {
|
|
64
|
+
expect(resolveArtifactDisplayMode({ display: {} }, markdown)).toBe("panel");
|
|
38
65
|
expect(
|
|
39
|
-
resolveArtifactDisplayMode({ display: {
|
|
66
|
+
resolveArtifactDisplayMode({ display: { byKind: { component: "inline" } } }, markdown)
|
|
40
67
|
).toBe("panel");
|
|
41
68
|
});
|
|
69
|
+
|
|
70
|
+
it("supports deprecated byType but lets canonical byKind win", () => {
|
|
71
|
+
const feature = {
|
|
72
|
+
display: {
|
|
73
|
+
byType: { component: "collapsed" as const },
|
|
74
|
+
byKind: { component: "inline" as const },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
expect(resolveArtifactDisplayMode(feature, component)).toBe("inline");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("resolves exact MIME, wildcard MIME, files default, kind, and default precedence", () => {
|
|
81
|
+
const feature = {
|
|
82
|
+
display: {
|
|
83
|
+
default: "collapsed" as const,
|
|
84
|
+
byKind: { component: "panel" as const },
|
|
85
|
+
files: {
|
|
86
|
+
default: "collapsed" as const,
|
|
87
|
+
byMediaType: {
|
|
88
|
+
"text/html": "inline" as const,
|
|
89
|
+
"image/*": "panel" as const,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
expect(
|
|
95
|
+
resolveArtifactDisplayMode(feature, {
|
|
96
|
+
artifactType: "markdown",
|
|
97
|
+
file: { path: "a.html", mimeType: "Text/HTML; charset=utf-8" },
|
|
98
|
+
})
|
|
99
|
+
).toBe("inline");
|
|
100
|
+
expect(
|
|
101
|
+
resolveArtifactDisplayMode(feature, {
|
|
102
|
+
artifactType: "markdown",
|
|
103
|
+
file: { path: "chart.png", mimeType: "image/png" },
|
|
104
|
+
})
|
|
105
|
+
).toBe("panel");
|
|
106
|
+
expect(
|
|
107
|
+
resolveArtifactDisplayMode(feature, {
|
|
108
|
+
artifactType: "markdown",
|
|
109
|
+
file: { path: "a.txt", mimeType: "text/plain" },
|
|
110
|
+
})
|
|
111
|
+
).toBe("collapsed");
|
|
112
|
+
expect(resolveArtifactDisplayMode(feature, component)).toBe("panel");
|
|
113
|
+
expect(resolveArtifactDisplayMode(feature, markdown)).toBe("collapsed");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("prefers an exact MIME rule over a wildcard for the same major type", () => {
|
|
117
|
+
const feature = {
|
|
118
|
+
display: {
|
|
119
|
+
files: {
|
|
120
|
+
byMediaType: {
|
|
121
|
+
"image/*": "collapsed" as const,
|
|
122
|
+
"image/svg+xml": "inline" as const,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
expect(
|
|
128
|
+
resolveArtifactDisplayMode(feature, {
|
|
129
|
+
artifactType: "markdown",
|
|
130
|
+
file: { path: "logo.svg", mimeType: "image/svg+xml" },
|
|
131
|
+
})
|
|
132
|
+
).toBe("inline");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("applies a files string to every file-backed artifact", () => {
|
|
136
|
+
const feature = { display: { files: "inline" as const } };
|
|
137
|
+
expect(
|
|
138
|
+
resolveArtifactDisplayMode(feature, {
|
|
139
|
+
artifactType: "markdown",
|
|
140
|
+
file: { path: "a.csv", mimeType: "text/csv" },
|
|
141
|
+
})
|
|
142
|
+
).toBe("inline");
|
|
143
|
+
expect(resolveArtifactDisplayMode(feature, markdown)).toBe("panel");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("honors the producer preferredMode hint over defaults but not explicit rules", () => {
|
|
147
|
+
const hinted = {
|
|
148
|
+
artifactType: "markdown" as const,
|
|
149
|
+
presentation: { preferredMode: "inline" as const },
|
|
150
|
+
};
|
|
151
|
+
expect(resolveArtifactDisplayMode(undefined, hinted)).toBe("inline");
|
|
152
|
+
expect(resolveArtifactDisplayMode({ display: "collapsed" }, hinted)).toBe(
|
|
153
|
+
"inline"
|
|
154
|
+
);
|
|
155
|
+
expect(
|
|
156
|
+
resolveArtifactDisplayMode({ display: { default: "collapsed" } }, hinted)
|
|
157
|
+
).toBe("inline");
|
|
158
|
+
expect(
|
|
159
|
+
resolveArtifactDisplayMode(
|
|
160
|
+
{ display: { byKind: { markdown: "panel" } } },
|
|
161
|
+
hinted
|
|
162
|
+
)
|
|
163
|
+
).toBe("panel");
|
|
164
|
+
expect(
|
|
165
|
+
resolveArtifactDisplayMode(
|
|
166
|
+
{ display: { files: "panel" } },
|
|
167
|
+
{ ...hinted, file: { path: "a.txt", mimeType: "text/plain" } }
|
|
168
|
+
)
|
|
169
|
+
).toBe("panel");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns the winning rule and configured selector for explainable host UI", () => {
|
|
173
|
+
expect(
|
|
174
|
+
resolveArtifactDisplay(
|
|
175
|
+
{
|
|
176
|
+
display: {
|
|
177
|
+
files: {
|
|
178
|
+
default: "panel",
|
|
179
|
+
byMediaType: { "text/html": "inline" },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
artifactType: "markdown",
|
|
185
|
+
file: { path: "a.html", mimeType: "Text/HTML; charset=utf-8" },
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
).toEqual({
|
|
189
|
+
mode: "inline",
|
|
190
|
+
matchedBy: {
|
|
191
|
+
type: "mediaType",
|
|
192
|
+
mediaType: "text/html",
|
|
193
|
+
selector: "text/html",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
expect(
|
|
197
|
+
resolveArtifactDisplay(
|
|
198
|
+
{ display: { files: { byMediaType: { "image/*": "inline" } } } },
|
|
199
|
+
{
|
|
200
|
+
artifactType: "markdown",
|
|
201
|
+
file: { path: "a.png", mimeType: "image/png" },
|
|
202
|
+
}
|
|
203
|
+
).matchedBy
|
|
204
|
+
).toEqual({ type: "mediaType", mediaType: "image/png", selector: "image/*" });
|
|
205
|
+
});
|
|
42
206
|
});
|
|
@@ -1,27 +1,166 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentWidgetArtifactsFeature,
|
|
3
3
|
PersonaArtifactDisplayMode,
|
|
4
|
-
|
|
4
|
+
PersonaArtifactDisplayModeInput,
|
|
5
|
+
PersonaArtifactFileMeta,
|
|
6
|
+
PersonaArtifactKind,
|
|
7
|
+
PersonaArtifactPresentation,
|
|
8
|
+
PersonaArtifactRecord,
|
|
5
9
|
} from "../types";
|
|
6
10
|
|
|
11
|
+
/** Canonicalize the deprecated `"card"` alias to `"collapsed"`. */
|
|
12
|
+
export function canonicalArtifactDisplayMode(
|
|
13
|
+
mode: PersonaArtifactDisplayModeInput
|
|
14
|
+
): PersonaArtifactDisplayMode {
|
|
15
|
+
return mode === "card" ? "collapsed" : mode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PersonaArtifactDisplayDescriptor = Pick<
|
|
19
|
+
PersonaArtifactRecord,
|
|
20
|
+
"artifactType" | "file" | "presentation"
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export type PersonaArtifactDisplayMatch =
|
|
24
|
+
/** `selector` is the configured `byMediaType` key ("text/html" or "image/*"). */
|
|
25
|
+
| { type: "mediaType"; mediaType: string; selector: string }
|
|
26
|
+
| { type: "files" }
|
|
27
|
+
| {
|
|
28
|
+
type: "kind";
|
|
29
|
+
kind: PersonaArtifactKind;
|
|
30
|
+
configuredBy: "byKind" | "byType";
|
|
31
|
+
}
|
|
32
|
+
| { type: "preferredMode" }
|
|
33
|
+
| { type: "default" }
|
|
34
|
+
| { type: "personaDefault" };
|
|
35
|
+
|
|
36
|
+
export type PersonaArtifactDisplayResolution = {
|
|
37
|
+
mode: PersonaArtifactDisplayMode;
|
|
38
|
+
matchedBy: PersonaArtifactDisplayMatch;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function normalizeMediaType(mediaType: string): string {
|
|
42
|
+
return mediaType.split(";", 1)[0].trim().toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function matchMediaType(
|
|
46
|
+
byMediaType: Record<string, PersonaArtifactDisplayModeInput> | undefined,
|
|
47
|
+
mediaType: string
|
|
48
|
+
): { mode: PersonaArtifactDisplayMode; selector: string } | undefined {
|
|
49
|
+
if (!byMediaType) return undefined;
|
|
50
|
+
const entries = Object.entries(byMediaType);
|
|
51
|
+
for (const [candidate, mode] of entries) {
|
|
52
|
+
if (normalizeMediaType(candidate) === mediaType) {
|
|
53
|
+
return {
|
|
54
|
+
mode: canonicalArtifactDisplayMode(mode),
|
|
55
|
+
selector: normalizeMediaType(candidate),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const wildcard = `${mediaType.split("/", 1)[0]}/*`;
|
|
60
|
+
for (const [candidate, mode] of entries) {
|
|
61
|
+
if (normalizeMediaType(candidate) === wildcard) {
|
|
62
|
+
return { mode: canonicalArtifactDisplayMode(mode), selector: wildcard };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
7
68
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
69
|
+
* Resolve an artifact's display mode together with the rule that selected it.
|
|
70
|
+
* Always returns a canonical mode ("card" inputs resolve as "collapsed").
|
|
10
71
|
*
|
|
11
|
-
* Resolution order: `
|
|
12
|
-
*
|
|
72
|
+
* Resolution order: file rules (exact MIME, then `major/*` wildcard, then the
|
|
73
|
+
* files default), kind, the producer's `presentation.preferredMode` hint,
|
|
74
|
+
* configured default, then `"panel"`.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveArtifactDisplay(
|
|
77
|
+
feature: AgentWidgetArtifactsFeature | undefined,
|
|
78
|
+
artifact: PersonaArtifactDisplayDescriptor
|
|
79
|
+
): PersonaArtifactDisplayResolution {
|
|
80
|
+
const display = feature?.display;
|
|
81
|
+
const rules =
|
|
82
|
+
typeof display === "string" ? { default: display } : display ?? {};
|
|
83
|
+
|
|
84
|
+
if (artifact.file && rules.files) {
|
|
85
|
+
if (typeof rules.files === "string") {
|
|
86
|
+
return {
|
|
87
|
+
mode: canonicalArtifactDisplayMode(rules.files),
|
|
88
|
+
matchedBy: { type: "files" },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const mediaType = artifact.file.mimeType
|
|
92
|
+
? normalizeMediaType(artifact.file.mimeType)
|
|
93
|
+
: undefined;
|
|
94
|
+
const byMediaType = mediaType
|
|
95
|
+
? matchMediaType(rules.files.byMediaType, mediaType)
|
|
96
|
+
: undefined;
|
|
97
|
+
if (mediaType && byMediaType) {
|
|
98
|
+
return {
|
|
99
|
+
mode: byMediaType.mode,
|
|
100
|
+
matchedBy: {
|
|
101
|
+
type: "mediaType",
|
|
102
|
+
mediaType,
|
|
103
|
+
selector: byMediaType.selector,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (rules.files.default) {
|
|
108
|
+
return {
|
|
109
|
+
mode: canonicalArtifactDisplayMode(rules.files.default),
|
|
110
|
+
matchedBy: { type: "files" },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const byKind = rules.byKind?.[artifact.artifactType];
|
|
116
|
+
if (byKind) {
|
|
117
|
+
return {
|
|
118
|
+
mode: canonicalArtifactDisplayMode(byKind),
|
|
119
|
+
matchedBy: {
|
|
120
|
+
type: "kind",
|
|
121
|
+
kind: artifact.artifactType,
|
|
122
|
+
configuredBy: "byKind",
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const byType = rules.byType?.[artifact.artifactType];
|
|
127
|
+
if (byType) {
|
|
128
|
+
return {
|
|
129
|
+
mode: canonicalArtifactDisplayMode(byType),
|
|
130
|
+
matchedBy: {
|
|
131
|
+
type: "kind",
|
|
132
|
+
kind: artifact.artifactType,
|
|
133
|
+
configuredBy: "byType",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const preferred = artifact.presentation?.preferredMode;
|
|
139
|
+
if (preferred) {
|
|
140
|
+
return {
|
|
141
|
+
mode: canonicalArtifactDisplayMode(preferred),
|
|
142
|
+
matchedBy: { type: "preferredMode" },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (rules.default) {
|
|
147
|
+
return {
|
|
148
|
+
mode: canonicalArtifactDisplayMode(rules.default),
|
|
149
|
+
matchedBy: { type: "default" },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { mode: "panel", matchedBy: { type: "personaDefault" } };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve only the mode. Use `resolveArtifactDisplay` when a host needs to
|
|
157
|
+
* explain which rule selected it.
|
|
13
158
|
*/
|
|
14
159
|
export function resolveArtifactDisplayMode(
|
|
15
160
|
feature: AgentWidgetArtifactsFeature | undefined,
|
|
16
|
-
|
|
161
|
+
artifact: PersonaArtifactDisplayDescriptor
|
|
17
162
|
): PersonaArtifactDisplayMode {
|
|
18
|
-
|
|
19
|
-
if (!display) return "panel";
|
|
20
|
-
if (typeof display === "string") return display;
|
|
21
|
-
const byType = display.byType as
|
|
22
|
-
| Partial<Record<string, PersonaArtifactDisplayMode>>
|
|
23
|
-
| undefined;
|
|
24
|
-
return byType?.[artifactType] ?? display.default ?? "panel";
|
|
163
|
+
return resolveArtifactDisplay(feature, artifact).mode;
|
|
25
164
|
}
|
|
26
165
|
|
|
27
166
|
/** Props embedded in an in-thread artifact block's rawContent JSON. */
|
|
@@ -32,6 +171,7 @@ export type ArtifactRefBlockProps = {
|
|
|
32
171
|
status: "streaming" | "complete";
|
|
33
172
|
/** File metadata for previewable file artifacts (persists the download path). */
|
|
34
173
|
file?: PersonaArtifactFileMeta;
|
|
174
|
+
presentation?: PersonaArtifactPresentation;
|
|
35
175
|
/**
|
|
36
176
|
* Component name for component-type artifacts. Only embedded for the
|
|
37
177
|
* "inline" display mode: the inline block renders component artifacts
|
|
@@ -54,7 +194,7 @@ export type ArtifactRefBlockProps = {
|
|
|
54
194
|
* Builds the `rawContent` JSON for the synthetic assistant message that
|
|
55
195
|
* anchors an artifact in the transcript (message id `artifact-ref-<id>`).
|
|
56
196
|
*
|
|
57
|
-
* "
|
|
197
|
+
* "collapsed"/"panel" modes render the compact `PersonaArtifactCard`; "inline"
|
|
58
198
|
* renders the `PersonaArtifactInline` preview block. Both share this shape so
|
|
59
199
|
* transcript persistence and hydration work unchanged. Used by the streaming
|
|
60
200
|
* client (`artifact_start`) and the programmatic `session.upsertArtifact()`.
|
|
@@ -72,6 +212,7 @@ export function buildArtifactRefRawContent(
|
|
|
72
212
|
artifactType: props.artifactType,
|
|
73
213
|
status: props.status,
|
|
74
214
|
...(props.file ? { file: props.file } : {}),
|
|
215
|
+
...(props.presentation ? { presentation: props.presentation } : {}),
|
|
75
216
|
...(displayMode === "inline" && props.component
|
|
76
217
|
? { component: props.component }
|
|
77
218
|
: {}),
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
// Canonical cross-repo fixture. The zero-width space is written as a
|
|
11
11
|
// backslash-u200b escape sequence (not a literal invisible char) so the bytes
|
|
12
12
|
// are unambiguous in source.
|
|
13
|
-
// Mirrors core/packages/shared/src/file-artifact.ts escapeFenceTerminators fixture — keep
|
|
13
|
+
// Mirrors core/packages/shared/src/file-artifact.ts escapeFenceTerminators fixture — keep in sync.
|
|
14
14
|
const ZWSP = "\u200b";
|
|
15
15
|
const FIXTURE_RAW = "# Title\n\n```js\nconsole.log('hi')\n```\n";
|
|
16
16
|
const FIXTURE_ESCAPED =
|
|
@@ -563,6 +563,46 @@ describe("Full Config with Hooks Integration", () => {
|
|
|
563
563
|
// Backward Compatibility Tests
|
|
564
564
|
// =============================================================================
|
|
565
565
|
|
|
566
|
+
describe("Suggestions Serialization", () => {
|
|
567
|
+
it("preserves structured suggestion configuration in every format", () => {
|
|
568
|
+
const config = {
|
|
569
|
+
...minimalConfig,
|
|
570
|
+
suggestions: {
|
|
571
|
+
starters: {
|
|
572
|
+
variant: "card",
|
|
573
|
+
behavior: "fill",
|
|
574
|
+
items: [
|
|
575
|
+
{
|
|
576
|
+
id: "compare",
|
|
577
|
+
label: "Compare plans",
|
|
578
|
+
prompt: "Compare plans for my team",
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
followUps: {
|
|
583
|
+
placement: "after-message",
|
|
584
|
+
overflow: "scroll",
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
const formats: CodeFormat[] = [
|
|
589
|
+
"esm",
|
|
590
|
+
"react-component",
|
|
591
|
+
"react-advanced",
|
|
592
|
+
"script-installer",
|
|
593
|
+
"script-manual",
|
|
594
|
+
"script-advanced",
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
for (const format of formats) {
|
|
598
|
+
const code = generateCodeSnippet(config, format);
|
|
599
|
+
expect(code).toContain("suggestions");
|
|
600
|
+
expect(code).toContain("Compare plans");
|
|
601
|
+
expect(code).toContain("after-message");
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
566
606
|
describe("Backward Compatibility", () => {
|
|
567
607
|
it("should work without options parameter", () => {
|
|
568
608
|
const code = generateCodeSnippet(minimalConfig, "esm");
|
|
@@ -430,6 +430,19 @@ function generateMarkdownConfig(config: any, indent: string): string[] {
|
|
|
430
430
|
return lines;
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
// Suggestions may contain nested rich items. JSON object syntax is also valid
|
|
434
|
+
// JavaScript/TypeScript, so preserve the complete public shape instead of
|
|
435
|
+
// duplicating every leaf in each output-format generator.
|
|
436
|
+
function generateSuggestionsConfig(config: any, indent: string): string[] {
|
|
437
|
+
if (!config.suggestions) return [];
|
|
438
|
+
const serialized = JSON.stringify(config.suggestions, null, 2).split("\n");
|
|
439
|
+
return serialized.map((line, index) => {
|
|
440
|
+
const prefix = index === 0 ? `${indent}suggestions: ` : indent;
|
|
441
|
+
const suffix = index === serialized.length - 1 ? "," : "";
|
|
442
|
+
return `${prefix}${line}${suffix}`;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
433
446
|
// Helper to generate layout config (excluding render functions and slots)
|
|
434
447
|
function generateLayoutConfig(config: any, indent: string): string[] {
|
|
435
448
|
const lines: string[] = [];
|
|
@@ -697,6 +710,8 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
|
|
|
697
710
|
lines.push(" },");
|
|
698
711
|
}
|
|
699
712
|
|
|
713
|
+
lines.push(...generateSuggestionsConfig(config, " "));
|
|
714
|
+
|
|
700
715
|
if (config.suggestionChips && config.suggestionChips.length > 0) {
|
|
701
716
|
lines.push(" suggestionChips: [");
|
|
702
717
|
config.suggestionChips.forEach((chip: string) => {
|
|
@@ -845,6 +860,8 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
|
|
|
845
860
|
lines.push(" },");
|
|
846
861
|
}
|
|
847
862
|
|
|
863
|
+
lines.push(...generateSuggestionsConfig(config, " "));
|
|
864
|
+
|
|
848
865
|
if (config.suggestionChips && config.suggestionChips.length > 0) {
|
|
849
866
|
lines.push(" suggestionChips: [");
|
|
850
867
|
config.suggestionChips.forEach((chip: string) => {
|
|
@@ -1110,6 +1127,8 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
|
|
|
1110
1127
|
lines.push(" },");
|
|
1111
1128
|
}
|
|
1112
1129
|
|
|
1130
|
+
lines.push(...generateSuggestionsConfig(config, " "));
|
|
1131
|
+
|
|
1113
1132
|
if (config.suggestionChips && config.suggestionChips.length > 0) {
|
|
1114
1133
|
lines.push(" suggestionChips: [");
|
|
1115
1134
|
config.suggestionChips.forEach((chip: string) => {
|
|
@@ -1312,6 +1331,7 @@ function buildSerializableConfig(config: any): Record<string, any> {
|
|
|
1312
1331
|
if (config.voiceRecognition) serializableConfig.voiceRecognition = config.voiceRecognition;
|
|
1313
1332
|
if (config.statusIndicator) serializableConfig.statusIndicator = config.statusIndicator;
|
|
1314
1333
|
if (config.features) serializableConfig.features = config.features;
|
|
1334
|
+
if (config.suggestions) serializableConfig.suggestions = config.suggestions;
|
|
1315
1335
|
if (config.suggestionChips?.length > 0) serializableConfig.suggestionChips = config.suggestionChips;
|
|
1316
1336
|
if (config.suggestionChipsConfig) serializableConfig.suggestionChipsConfig = config.suggestionChipsConfig;
|
|
1317
1337
|
if (config.debug) serializableConfig.debug = config.debug;
|
|
@@ -1520,6 +1540,8 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
|
|
|
1520
1540
|
lines.push(" },");
|
|
1521
1541
|
}
|
|
1522
1542
|
|
|
1543
|
+
lines.push(...generateSuggestionsConfig(config, " "));
|
|
1544
|
+
|
|
1523
1545
|
if (config.suggestionChips && config.suggestionChips.length > 0) {
|
|
1524
1546
|
lines.push(" suggestionChips: [");
|
|
1525
1547
|
config.suggestionChips.forEach((chip: string) => {
|
|
@@ -32,6 +32,30 @@ describe("mergeConfigUpdate", () => {
|
|
|
32
32
|
expect(next.suggestionChips).toEqual(["x"]);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it("patches suggestion surfaces without dropping sibling behavior", () => {
|
|
36
|
+
const prev = base({
|
|
37
|
+
suggestions: {
|
|
38
|
+
starters: { variant: "card", behavior: "fill" },
|
|
39
|
+
followUps: { variant: "chip", placement: "after-message" },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const next = mergeConfigUpdate(prev, {
|
|
43
|
+
suggestions: {
|
|
44
|
+
followUps: { overflow: "wrap" },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(next.suggestions?.starters).toEqual({
|
|
49
|
+
variant: "card",
|
|
50
|
+
behavior: "fill",
|
|
51
|
+
});
|
|
52
|
+
expect(next.suggestions?.followUps).toEqual({
|
|
53
|
+
variant: "chip",
|
|
54
|
+
placement: "after-message",
|
|
55
|
+
overflow: "wrap",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
35
59
|
it("replaces callbacks wholesale", () => {
|
|
36
60
|
const first = vi.fn();
|
|
37
61
|
const second = vi.fn();
|
|
@@ -66,6 +90,66 @@ describe("mergeConfigUpdate", () => {
|
|
|
66
90
|
expect(next.features?.streamAnimation?.plugins?.p1).toBeUndefined();
|
|
67
91
|
});
|
|
68
92
|
|
|
93
|
+
it("replaces artifact display scope maps so reset preferences do not strand stale keys", () => {
|
|
94
|
+
const previous = mergeWithDefaults({
|
|
95
|
+
features: {
|
|
96
|
+
artifacts: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
display: {
|
|
99
|
+
default: "card",
|
|
100
|
+
files: {
|
|
101
|
+
byMediaType: {
|
|
102
|
+
"text/html": "inline",
|
|
103
|
+
"text/csv": "inline",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const next = mergeConfigUpdate(previous, {
|
|
112
|
+
features: {
|
|
113
|
+
artifacts: {
|
|
114
|
+
display: {
|
|
115
|
+
default: "card",
|
|
116
|
+
files: {
|
|
117
|
+
byMediaType: {
|
|
118
|
+
"text/csv": "panel",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(next.features?.artifacts?.display).toEqual({
|
|
127
|
+
default: "card",
|
|
128
|
+
files: {
|
|
129
|
+
byMediaType: {
|
|
130
|
+
"text/csv": "panel",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("replaces the preferences slice wholesale so dropped overrides do not strand", () => {
|
|
137
|
+
const prev = base({
|
|
138
|
+
preferences: {
|
|
139
|
+
showToolCalls: false,
|
|
140
|
+
artifacts: { display: "inline", filePreview: { enabled: false } },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
// The host's new slice dropped showToolCalls and filePreview: both must go.
|
|
144
|
+
const next = mergeConfigUpdate(prev, {
|
|
145
|
+
preferences: { artifacts: { display: "inline" } },
|
|
146
|
+
});
|
|
147
|
+
expect(next.preferences).toEqual({ artifacts: { display: "inline" } });
|
|
148
|
+
// Explicit-undefined still clears the whole slice.
|
|
149
|
+
const cleared = mergeConfigUpdate(next, { preferences: undefined });
|
|
150
|
+
expect(cleared.preferences).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
69
153
|
it("replaces storageAdapter wholesale (no hybrid: new adapter's absent save is not inherited)", () => {
|
|
70
154
|
const oldSave = vi.fn();
|
|
71
155
|
const oldLoad = vi.fn();
|
|
@@ -9,9 +9,12 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
|
9
9
|
|
|
10
10
|
// Dotted paths whose plain-object value replaces wholesale instead of recursing.
|
|
11
11
|
// - user-defined key maps: recursive merge would strand stale keys from the old value
|
|
12
|
-
// (headers, targetProviders, components,
|
|
12
|
+
// (headers, targetProviders, components, artifact display scopes,
|
|
13
|
+
// features.streamAnimation.plugins).
|
|
13
14
|
// - implementation objects: partial merge splices two impls into a broken hybrid
|
|
14
15
|
// (agent, storageAdapter, voiceRecognition.provider.custom).
|
|
16
|
+
// - preferences: the sparse slice a host passes IS its full opinion; recursive
|
|
17
|
+
// merge would keep overrides the host dropped from its slice.
|
|
15
18
|
const REPLACE_LEAF_PATHS = new Set<string>([
|
|
16
19
|
"headers",
|
|
17
20
|
"agent",
|
|
@@ -19,7 +22,9 @@ const REPLACE_LEAF_PATHS = new Set<string>([
|
|
|
19
22
|
"components",
|
|
20
23
|
"targetProviders",
|
|
21
24
|
"voiceRecognition.provider.custom",
|
|
25
|
+
"features.artifacts.display",
|
|
22
26
|
"features.streamAnimation.plugins",
|
|
27
|
+
"preferences",
|
|
23
28
|
]);
|
|
24
29
|
|
|
25
30
|
// Recursive patch merge. A key merges recursively iff both previous and patch
|