@runtypelabs/persona 4.7.0 → 4.8.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/animations/glyph-cycle.cjs +2 -2
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -2
- package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
- package/dist/animations/wipe.cjs +2 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-5EIIHQLQ.js +1 -0
- package/dist/codegen.cjs +12 -12
- package/dist/codegen.js +14 -14
- package/dist/index.cjs +91 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +877 -172
- package/dist/index.d.ts +877 -172
- package/dist/index.global.js +81 -57
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +91 -68
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +24 -24
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-reconnect-JKIJBHS5.js +1 -0
- package/dist/smart-dom-reader.cjs +17 -17
- package/dist/smart-dom-reader.d.cts +753 -15
- package/dist/smart-dom-reader.d.ts +753 -15
- package/dist/smart-dom-reader.js +17 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +81 -58
- package/dist/theme-editor-preview.d.cts +761 -15
- package/dist/theme-editor-preview.d.ts +761 -15
- package/dist/theme-editor-preview.js +81 -58
- package/dist/theme-editor.cjs +6 -6
- package/dist/theme-editor.d.cts +753 -15
- package/dist/theme-editor.d.ts +753 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +74 -0
- package/dist/theme-reference.d.ts +74 -0
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +2 -2
- package/dist/voice-worklet-player.js +2 -2
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +2 -3
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +186 -1
- package/src/client.ts +92 -9
- package/src/components/artifact-card.test.ts +333 -0
- package/src/components/artifact-card.ts +75 -28
- package/src/components/artifact-inline.test.ts +1328 -0
- package/src/components/artifact-inline.ts +920 -0
- package/src/components/artifact-pane.test.ts +1042 -0
- package/src/components/artifact-pane.ts +440 -131
- package/src/components/artifact-preview.test.ts +1155 -0
- package/src/components/artifact-preview.ts +994 -0
- package/src/components/pill-composer-builder.test.ts +6 -2
- package/src/components/pill-composer-builder.ts +6 -6
- package/src/components/reasoning-bubble.ts +1 -13
- package/src/components/registry.ts +38 -3
- package/src/components/tool-bubble.ts +1 -13
- package/src/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +20 -1
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +74 -9
- package/src/runtime/host-layout.test.ts +163 -0
- package/src/runtime/host-layout.ts +110 -7
- package/src/runtime/init.ts +18 -61
- package/src/session.ts +76 -22
- package/src/styles/widget.css +773 -26
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +10 -2
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +77 -0
- package/src/types.ts +516 -17
- package/src/ui.artifact-pane-gating.test.ts +636 -0
- package/src/ui.component-directive.test.ts +104 -0
- package/src/ui.composer-bar.test.ts +60 -2
- package/src/ui.detached-panel.test.ts +1049 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +698 -109
- package/src/utils/artifact-custom-actions.ts +128 -0
- package/src/utils/artifact-display.test.ts +42 -0
- package/src/utils/artifact-display.ts +84 -0
- package/src/utils/artifact-file.test.ts +116 -0
- package/src/utils/artifact-file.ts +117 -0
- package/src/utils/artifact-gate.test.ts +112 -5
- package/src/utils/artifact-gate.ts +39 -14
- package/src/utils/artifact-loading-status.ts +55 -0
- package/src/utils/artifact-status-label.ts +190 -0
- package/src/utils/buttons.ts +7 -1
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/icons.ts +2 -0
- package/src/utils/roving-tablist.test.ts +152 -0
- package/src/utils/roving-tablist.ts +111 -0
- package/src/utils/spinner.ts +45 -0
- package/src/utils/theme.test.ts +48 -0
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +91 -0
- package/src/utils/tool-loading-animation.test.ts +32 -0
- package/src/utils/tool-loading-animation.ts +24 -0
- package/dist/chunk-DFBSCFYN.js +0 -1
- package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
- package/dist/session-reconnect-U77QFUR7.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -74,8 +74,7 @@
|
|
|
74
74
|
"idiomorph": "^0.7.4",
|
|
75
75
|
"lucide": "^1.18.0",
|
|
76
76
|
"marked": "^12.0.2",
|
|
77
|
-
"partial-json": "^0.1.7"
|
|
78
|
-
"zod": "^3.22.4"
|
|
77
|
+
"partial-json": "^0.1.7"
|
|
79
78
|
},
|
|
80
79
|
"devDependencies": {
|
|
81
80
|
"@size-limit/file": "^12.1.0",
|
|
@@ -77,4 +77,182 @@ describe("AgentWidgetSession artifacts", () => {
|
|
|
77
77
|
expect(session.getArtifacts()).toHaveLength(1);
|
|
78
78
|
expect(session.getArtifacts()[0].markdown).toBe("C");
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
it("stores file metadata via applyArtifactStreamEvent and keeps accumulating deltas", () => {
|
|
82
|
+
const session = new AgentWidgetSession(
|
|
83
|
+
{},
|
|
84
|
+
{
|
|
85
|
+
onMessagesChanged: () => {},
|
|
86
|
+
onStatusChanged: () => {},
|
|
87
|
+
onStreamingChanged: () => {},
|
|
88
|
+
onArtifactsState: () => {}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
const file = { path: "outputs/cat.html", mimeType: "text/html", language: "html" };
|
|
92
|
+
session.injectTestEvent({
|
|
93
|
+
type: "artifact_start",
|
|
94
|
+
id: "f1",
|
|
95
|
+
artifactType: "markdown",
|
|
96
|
+
title: "outputs/cat.html",
|
|
97
|
+
file
|
|
98
|
+
});
|
|
99
|
+
session.injectTestEvent({ type: "artifact_delta", id: "f1", artDelta: "```html\n" });
|
|
100
|
+
session.injectTestEvent({ type: "artifact_delta", id: "f1", artDelta: "<h1>hi</h1>\n```" });
|
|
101
|
+
const rec = session.getArtifactById("f1");
|
|
102
|
+
expect(rec?.file).toEqual(file);
|
|
103
|
+
expect(rec?.markdown).toBe("```html\n<h1>hi</h1>\n```");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("upsertArtifact injects a card transcript block by default", () => {
|
|
107
|
+
const session = new AgentWidgetSession(
|
|
108
|
+
{},
|
|
109
|
+
{
|
|
110
|
+
onMessagesChanged: () => {},
|
|
111
|
+
onStatusChanged: () => {},
|
|
112
|
+
onStreamingChanged: () => {},
|
|
113
|
+
onArtifactsState: () => {}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
const rec = session.upsertArtifact({
|
|
117
|
+
artifactType: "markdown",
|
|
118
|
+
title: "Doc",
|
|
119
|
+
content: "# Hi"
|
|
120
|
+
});
|
|
121
|
+
const block = session
|
|
122
|
+
.getMessages()
|
|
123
|
+
.find((m) => m.id === `artifact-ref-${rec.id}`);
|
|
124
|
+
expect(block).toBeDefined();
|
|
125
|
+
expect(block?.role).toBe("assistant");
|
|
126
|
+
const parsed = JSON.parse(block!.rawContent!);
|
|
127
|
+
expect(parsed.component).toBe("PersonaArtifactCard");
|
|
128
|
+
expect(parsed.props).toMatchObject({
|
|
129
|
+
artifactId: rec.id,
|
|
130
|
+
title: "Doc",
|
|
131
|
+
artifactType: "markdown",
|
|
132
|
+
status: "complete",
|
|
133
|
+
markdown: "# Hi"
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("upsertArtifact injects an inline block when display resolves to inline", () => {
|
|
138
|
+
const session = new AgentWidgetSession(
|
|
139
|
+
{ features: { artifacts: { enabled: true, display: "inline" } } },
|
|
140
|
+
{
|
|
141
|
+
onMessagesChanged: () => {},
|
|
142
|
+
onStatusChanged: () => {},
|
|
143
|
+
onStreamingChanged: () => {},
|
|
144
|
+
onArtifactsState: () => {}
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
const rec = session.upsertArtifact({
|
|
148
|
+
artifactType: "component",
|
|
149
|
+
title: "Chart",
|
|
150
|
+
component: "MyChart",
|
|
151
|
+
props: { series: [1, 2] }
|
|
152
|
+
});
|
|
153
|
+
const block = session
|
|
154
|
+
.getMessages()
|
|
155
|
+
.find((m) => m.id === `artifact-ref-${rec.id}`);
|
|
156
|
+
expect(block).toBeDefined();
|
|
157
|
+
const parsed = JSON.parse(block!.rawContent!);
|
|
158
|
+
expect(parsed.component).toBe("PersonaArtifactInline");
|
|
159
|
+
// Inline blocks render component artifacts through the registry, so the
|
|
160
|
+
// component name AND its props are embedded in the block props (the
|
|
161
|
+
// registry is not persisted, so hydration re-invokes the renderer from
|
|
162
|
+
// these).
|
|
163
|
+
expect(parsed.props.component).toBe("MyChart");
|
|
164
|
+
expect(parsed.props.componentProps).toEqual({ series: [1, 2] });
|
|
165
|
+
expect(parsed.props.status).toBe("complete");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("upsertArtifact with transcript: false injects no transcript block", () => {
|
|
169
|
+
const session = new AgentWidgetSession(
|
|
170
|
+
{},
|
|
171
|
+
{
|
|
172
|
+
onMessagesChanged: () => {},
|
|
173
|
+
onStatusChanged: () => {},
|
|
174
|
+
onStreamingChanged: () => {},
|
|
175
|
+
onArtifactsState: () => {}
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
session.upsertArtifact({
|
|
179
|
+
id: "pane-only",
|
|
180
|
+
artifactType: "markdown",
|
|
181
|
+
content: "C",
|
|
182
|
+
transcript: false
|
|
183
|
+
});
|
|
184
|
+
expect(session.getArtifacts()).toHaveLength(1);
|
|
185
|
+
expect(session.getMessages()).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("upsertArtifact update to an existing artifact does not duplicate the block", () => {
|
|
189
|
+
const session = new AgentWidgetSession(
|
|
190
|
+
{},
|
|
191
|
+
{
|
|
192
|
+
onMessagesChanged: () => {},
|
|
193
|
+
onStatusChanged: () => {},
|
|
194
|
+
onStreamingChanged: () => {},
|
|
195
|
+
onArtifactsState: () => {}
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
session.upsertArtifact({ id: "a1", artifactType: "markdown", content: "v1" });
|
|
199
|
+
session.upsertArtifact({ id: "a1", artifactType: "markdown", content: "v2" });
|
|
200
|
+
const blocks = session
|
|
201
|
+
.getMessages()
|
|
202
|
+
.filter((m) => m.id === "artifact-ref-a1");
|
|
203
|
+
expect(blocks).toHaveLength(1);
|
|
204
|
+
expect(session.getArtifacts()).toHaveLength(1);
|
|
205
|
+
expect(session.getArtifactById("a1")?.markdown).toBe("v2");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("upsertArtifact re-upsert rebuilds the existing block's persisted props", () => {
|
|
209
|
+
const session = new AgentWidgetSession(
|
|
210
|
+
{},
|
|
211
|
+
{
|
|
212
|
+
onMessagesChanged: () => {},
|
|
213
|
+
onStatusChanged: () => {},
|
|
214
|
+
onStreamingChanged: () => {},
|
|
215
|
+
onArtifactsState: () => {}
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
session.upsertArtifact({
|
|
219
|
+
id: "a1",
|
|
220
|
+
artifactType: "markdown",
|
|
221
|
+
title: "T1",
|
|
222
|
+
content: "v1"
|
|
223
|
+
});
|
|
224
|
+
session.upsertArtifact({
|
|
225
|
+
id: "a1",
|
|
226
|
+
artifactType: "markdown",
|
|
227
|
+
title: "T2",
|
|
228
|
+
content: "v2"
|
|
229
|
+
});
|
|
230
|
+
const blocks = session
|
|
231
|
+
.getMessages()
|
|
232
|
+
.filter((m) => m.id === "artifact-ref-a1");
|
|
233
|
+
expect(blocks).toHaveLength(1);
|
|
234
|
+
const parsed = JSON.parse(blocks[0].rawContent!);
|
|
235
|
+
expect(parsed.props.markdown).toBe("v2");
|
|
236
|
+
expect(parsed.props.title).toBe("T2");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("stores file metadata via upsertArtifact", () => {
|
|
240
|
+
const session = new AgentWidgetSession(
|
|
241
|
+
{},
|
|
242
|
+
{
|
|
243
|
+
onMessagesChanged: () => {},
|
|
244
|
+
onStatusChanged: () => {},
|
|
245
|
+
onStreamingChanged: () => {},
|
|
246
|
+
onArtifactsState: () => {}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
const file = { path: "notes.md", mimeType: "text/markdown" };
|
|
250
|
+
session.upsertArtifact({
|
|
251
|
+
artifactType: "markdown",
|
|
252
|
+
title: "notes.md",
|
|
253
|
+
content: "```md\n# Hi\n\n```",
|
|
254
|
+
file
|
|
255
|
+
});
|
|
256
|
+
expect(session.getArtifactById(session.getArtifacts()[0].id)?.file).toEqual(file);
|
|
257
|
+
});
|
|
80
258
|
});
|
package/src/client.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { AgentWidgetClient, preferFinalStructuredContent } from './client';
|
|
3
|
-
import { AgentWidgetEvent, AgentWidgetMessage } from './types';
|
|
3
|
+
import { AgentWidgetEvent, AgentWidgetMessage, AgentWidgetArtifactsFeature } from './types';
|
|
4
4
|
import { createJsonStreamParser } from './utils/formatting';
|
|
5
5
|
import { VERSION } from './version';
|
|
6
6
|
import { createUnifiedEventWrite } from './utils/__fixtures__/unified-translator.oracle';
|
|
@@ -4357,3 +4357,188 @@ describe('AgentWidgetClient - Wire event vocabulary (default in 4.0)', () => {
|
|
|
4357
4357
|
}
|
|
4358
4358
|
});
|
|
4359
4359
|
});
|
|
4360
|
+
|
|
4361
|
+
describe('AgentWidgetClient - Artifact file metadata', () => {
|
|
4362
|
+
const artifactFrames = (fileField: string | null): string[] => [
|
|
4363
|
+
`data: {"type":"artifact_start","id":"a1","artifactType":"markdown","title":"outputs/cat.html"${
|
|
4364
|
+
fileField === null ? '' : ',"file":' + fileField
|
|
4365
|
+
}}\n\n`,
|
|
4366
|
+
`data: {"type":"artifact_delta","id":"a1","delta":${JSON.stringify('```html\n<h1>hi</h1>\n```')}}\n\n`,
|
|
4367
|
+
`data: {"type":"artifact_complete","id":"a1"}\n\n`,
|
|
4368
|
+
];
|
|
4369
|
+
|
|
4370
|
+
it('propagates valid file metadata to the artifact_start event and card props', async () => {
|
|
4371
|
+
global.fetch = createRawStreamFetch(
|
|
4372
|
+
artifactFrames('{"path":"outputs/cat.html","mimeType":"text/html","language":"html"}')
|
|
4373
|
+
);
|
|
4374
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
4375
|
+
const events: AgentWidgetEvent[] = [];
|
|
4376
|
+
await client.dispatch(
|
|
4377
|
+
{ messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
|
|
4378
|
+
(e) => events.push(e)
|
|
4379
|
+
);
|
|
4380
|
+
|
|
4381
|
+
const startEv = events.find((e) => e.type === 'artifact_start');
|
|
4382
|
+
expect(startEv).toBeDefined();
|
|
4383
|
+
if (startEv?.type === 'artifact_start') {
|
|
4384
|
+
expect(startEv.file).toEqual({
|
|
4385
|
+
path: 'outputs/cat.html',
|
|
4386
|
+
mimeType: 'text/html',
|
|
4387
|
+
language: 'html',
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
// The completed card message carries the file metadata in its rawContent props.
|
|
4392
|
+
const cardMsgs = events.filter(
|
|
4393
|
+
(e): e is Extract<AgentWidgetEvent, { type: 'message' }> =>
|
|
4394
|
+
e.type === 'message' && e.message.id === 'artifact-ref-a1'
|
|
4395
|
+
);
|
|
4396
|
+
const lastCard = cardMsgs[cardMsgs.length - 1];
|
|
4397
|
+
expect(lastCard).toBeDefined();
|
|
4398
|
+
const props = JSON.parse(lastCard.message.rawContent ?? '{}').props;
|
|
4399
|
+
expect(props.status).toBe('complete');
|
|
4400
|
+
expect(props.file).toEqual({
|
|
4401
|
+
path: 'outputs/cat.html',
|
|
4402
|
+
mimeType: 'text/html',
|
|
4403
|
+
language: 'html',
|
|
4404
|
+
});
|
|
4405
|
+
expect(props.markdown).toBe('```html\n<h1>hi</h1>\n```');
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4408
|
+
it('drops malformed file metadata without error', async () => {
|
|
4409
|
+
global.fetch = createRawStreamFetch(artifactFrames('{"path":123}'));
|
|
4410
|
+
const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000' });
|
|
4411
|
+
const events: AgentWidgetEvent[] = [];
|
|
4412
|
+
await client.dispatch(
|
|
4413
|
+
{ messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
|
|
4414
|
+
(e) => events.push(e)
|
|
4415
|
+
);
|
|
4416
|
+
|
|
4417
|
+
expect(events.some((e) => e.type === 'error')).toBe(false);
|
|
4418
|
+
const startEv = events.find((e) => e.type === 'artifact_start');
|
|
4419
|
+
expect(startEv).toBeDefined();
|
|
4420
|
+
if (startEv?.type === 'artifact_start') {
|
|
4421
|
+
expect(startEv.file).toBeUndefined();
|
|
4422
|
+
}
|
|
4423
|
+
const cardMsgs = events.filter(
|
|
4424
|
+
(e): e is Extract<AgentWidgetEvent, { type: 'message' }> =>
|
|
4425
|
+
e.type === 'message' && e.message.id === 'artifact-ref-a1'
|
|
4426
|
+
);
|
|
4427
|
+
const props = JSON.parse(cardMsgs[cardMsgs.length - 1].message.rawContent ?? '{}').props;
|
|
4428
|
+
expect(props.file).toBeUndefined();
|
|
4429
|
+
});
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4432
|
+
describe('AgentWidgetClient - Artifact display modes', () => {
|
|
4433
|
+
const markdownArtifactFrames = (id = 'a1'): string[] => [
|
|
4434
|
+
`data: {"type":"artifact_start","id":"${id}","artifactType":"markdown","title":"Notes"}\n\n`,
|
|
4435
|
+
`data: {"type":"artifact_delta","id":"${id}","delta":"# Hello"}\n\n`,
|
|
4436
|
+
`data: {"type":"artifact_complete","id":"${id}"}\n\n`,
|
|
4437
|
+
];
|
|
4438
|
+
const componentArtifactFrames = (id = 'c1'): string[] => [
|
|
4439
|
+
`data: {"type":"artifact_start","id":"${id}","artifactType":"component","title":"Chart","component":"MyChart"}\n\n`,
|
|
4440
|
+
`data: {"type":"artifact_complete","id":"${id}"}\n\n`,
|
|
4441
|
+
];
|
|
4442
|
+
|
|
4443
|
+
const dispatchAndCollect = async (
|
|
4444
|
+
frames: string[],
|
|
4445
|
+
artifacts?: AgentWidgetArtifactsFeature
|
|
4446
|
+
): Promise<AgentWidgetEvent[]> => {
|
|
4447
|
+
global.fetch = createRawStreamFetch(frames);
|
|
4448
|
+
const client = new AgentWidgetClient({
|
|
4449
|
+
apiUrl: 'http://localhost:8000',
|
|
4450
|
+
...(artifacts ? { features: { artifacts } } : {}),
|
|
4451
|
+
});
|
|
4452
|
+
const events: AgentWidgetEvent[] = [];
|
|
4453
|
+
await client.dispatch(
|
|
4454
|
+
{ messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
|
|
4455
|
+
(e) => events.push(e)
|
|
4456
|
+
);
|
|
4457
|
+
return events;
|
|
4458
|
+
};
|
|
4459
|
+
|
|
4460
|
+
const blockMessagesFor = (events: AgentWidgetEvent[], artifactId: string) =>
|
|
4461
|
+
events.filter(
|
|
4462
|
+
(e): e is Extract<AgentWidgetEvent, { type: 'message' }> =>
|
|
4463
|
+
e.type === 'message' && e.message.id === `artifact-ref-${artifactId}`
|
|
4464
|
+
);
|
|
4465
|
+
|
|
4466
|
+
const componentNameOf = (msg: AgentWidgetMessage): string =>
|
|
4467
|
+
JSON.parse(msg.rawContent ?? '{}').component;
|
|
4468
|
+
|
|
4469
|
+
it('injects PersonaArtifactCard when display is unset (default "panel")', async () => {
|
|
4470
|
+
const events = await dispatchAndCollect(markdownArtifactFrames());
|
|
4471
|
+
const msgs = blockMessagesFor(events, 'a1');
|
|
4472
|
+
expect(msgs.length).toBeGreaterThan(0);
|
|
4473
|
+
for (const m of msgs) {
|
|
4474
|
+
expect(componentNameOf(m.message)).toBe('PersonaArtifactCard');
|
|
4475
|
+
}
|
|
4476
|
+
});
|
|
4477
|
+
|
|
4478
|
+
it('injects PersonaArtifactCard for display: "card" and display: "panel"', async () => {
|
|
4479
|
+
for (const mode of ['card', 'panel'] as const) {
|
|
4480
|
+
const events = await dispatchAndCollect(markdownArtifactFrames(), {
|
|
4481
|
+
enabled: true,
|
|
4482
|
+
display: mode,
|
|
4483
|
+
});
|
|
4484
|
+
const msgs = blockMessagesFor(events, 'a1');
|
|
4485
|
+
expect(msgs.length).toBeGreaterThan(0);
|
|
4486
|
+
expect(componentNameOf(msgs[0].message)).toBe('PersonaArtifactCard');
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
|
|
4490
|
+
it('injects PersonaArtifactInline for display: "inline"', async () => {
|
|
4491
|
+
const events = await dispatchAndCollect(markdownArtifactFrames(), {
|
|
4492
|
+
enabled: true,
|
|
4493
|
+
display: 'inline',
|
|
4494
|
+
});
|
|
4495
|
+
const msgs = blockMessagesFor(events, 'a1');
|
|
4496
|
+
expect(msgs.length).toBeGreaterThan(0);
|
|
4497
|
+
for (const m of msgs) {
|
|
4498
|
+
expect(componentNameOf(m.message)).toBe('PersonaArtifactInline');
|
|
4499
|
+
}
|
|
4500
|
+
const startProps = JSON.parse(msgs[0].message.rawContent ?? '{}').props;
|
|
4501
|
+
expect(startProps).toMatchObject({
|
|
4502
|
+
artifactId: 'a1',
|
|
4503
|
+
artifactType: 'markdown',
|
|
4504
|
+
status: 'streaming',
|
|
4505
|
+
});
|
|
4506
|
+
});
|
|
4507
|
+
|
|
4508
|
+
it('resolves the mode per artifact type via display.byType', async () => {
|
|
4509
|
+
const display: AgentWidgetArtifactsFeature['display'] = {
|
|
4510
|
+
default: 'card',
|
|
4511
|
+
byType: { component: 'inline' },
|
|
4512
|
+
};
|
|
4513
|
+
|
|
4514
|
+
const mdEvents = await dispatchAndCollect(markdownArtifactFrames(), {
|
|
4515
|
+
enabled: true,
|
|
4516
|
+
display,
|
|
4517
|
+
});
|
|
4518
|
+
expect(componentNameOf(blockMessagesFor(mdEvents, 'a1')[0].message)).toBe(
|
|
4519
|
+
'PersonaArtifactCard'
|
|
4520
|
+
);
|
|
4521
|
+
|
|
4522
|
+
const compEvents = await dispatchAndCollect(componentArtifactFrames(), {
|
|
4523
|
+
enabled: true,
|
|
4524
|
+
display,
|
|
4525
|
+
});
|
|
4526
|
+
const compMsgs = blockMessagesFor(compEvents, 'c1');
|
|
4527
|
+
expect(componentNameOf(compMsgs[0].message)).toBe('PersonaArtifactInline');
|
|
4528
|
+
// Inline component artifacts carry the component name for the preview registry.
|
|
4529
|
+
expect(JSON.parse(compMsgs[0].message.rawContent ?? '{}').props.component).toBe('MyChart');
|
|
4530
|
+
});
|
|
4531
|
+
|
|
4532
|
+
it('embeds accumulated markdown and complete status in inline block props on artifact_complete', async () => {
|
|
4533
|
+
const events = await dispatchAndCollect(markdownArtifactFrames(), {
|
|
4534
|
+
enabled: true,
|
|
4535
|
+
display: 'inline',
|
|
4536
|
+
});
|
|
4537
|
+
const msgs = blockMessagesFor(events, 'a1');
|
|
4538
|
+
const last = msgs[msgs.length - 1];
|
|
4539
|
+
expect(last.message.streaming).toBe(false);
|
|
4540
|
+
const props = JSON.parse(last.message.rawContent ?? '{}').props;
|
|
4541
|
+
expect(props.status).toBe('complete');
|
|
4542
|
+
expect(props.markdown).toBe('# Hello');
|
|
4543
|
+
});
|
|
4544
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -21,10 +21,15 @@ import {
|
|
|
21
21
|
ClientFeedbackRequest,
|
|
22
22
|
ClientFeedbackType,
|
|
23
23
|
PersonaArtifactKind,
|
|
24
|
+
PersonaArtifactFileMeta,
|
|
24
25
|
ContentPart,
|
|
25
26
|
WebMcpConfirmHandler
|
|
26
27
|
} from "./types";
|
|
27
28
|
import { WebMcpBridge, computeClientToolsFingerprint, isWebMcpToolName } from "./webmcp-bridge";
|
|
29
|
+
import {
|
|
30
|
+
buildArtifactRefRawContent,
|
|
31
|
+
resolveArtifactDisplayMode
|
|
32
|
+
} from "./utils/artifact-display";
|
|
28
33
|
import { resolveTarget } from "./utils/target";
|
|
29
34
|
import { builtInClientToolsForDispatch } from "./ask-user-question-tool";
|
|
30
35
|
import {
|
|
@@ -1701,12 +1706,25 @@ export class AgentWidgetClient {
|
|
|
1701
1706
|
|
|
1702
1707
|
// Track tool call IDs for artifact emit tools so we can suppress their UI
|
|
1703
1708
|
const artifactToolCallIds = new Set<string>();
|
|
1704
|
-
// Track artifact reference card
|
|
1709
|
+
// Track artifact block messages (reference card or inline block) so we can
|
|
1710
|
+
// update them on artifact_complete
|
|
1705
1711
|
const artifactCardMessages = new Map<string, AgentWidgetMessage>();
|
|
1706
1712
|
// Track artifact IDs that already have a reference card (from auto-creation or transcript_insert)
|
|
1707
1713
|
const artifactIdsWithCards = new Set<string>();
|
|
1708
|
-
// Accumulate artifact markdown content
|
|
1709
|
-
|
|
1714
|
+
// Accumulate artifact markdown content (and component props) for embedding
|
|
1715
|
+
// in block props on complete. `props` accumulates across `artifact_update`
|
|
1716
|
+
// events so an inline component block hydrates with its real props after a
|
|
1717
|
+
// refresh (the session artifact registry, which also tracks them live, is
|
|
1718
|
+
// not persisted).
|
|
1719
|
+
const artifactContent = new Map<
|
|
1720
|
+
string,
|
|
1721
|
+
{
|
|
1722
|
+
markdown: string;
|
|
1723
|
+
title?: string;
|
|
1724
|
+
file?: PersonaArtifactFileMeta;
|
|
1725
|
+
props?: Record<string, unknown>;
|
|
1726
|
+
}
|
|
1727
|
+
>();
|
|
1710
1728
|
const isArtifactEmitToolName = (name: string | undefined): boolean => {
|
|
1711
1729
|
if (!name) return false;
|
|
1712
1730
|
const normalized = name.replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
@@ -3105,17 +3123,57 @@ export class AgentWidgetClient {
|
|
|
3105
3123
|
const at = payload.artifactType as PersonaArtifactKind;
|
|
3106
3124
|
const artId = String(payload.id);
|
|
3107
3125
|
const artTitle = typeof payload.title === "string" ? payload.title : undefined;
|
|
3126
|
+
// Additive `file` metadata: validate shape (object with string path +
|
|
3127
|
+
// string mimeType; optional string language). Drop silently if malformed
|
|
3128
|
+
// so old/new backends and unexpected payloads never break the stream.
|
|
3129
|
+
const rawFile = payload.file;
|
|
3130
|
+
let artFile: PersonaArtifactFileMeta | undefined;
|
|
3131
|
+
if (
|
|
3132
|
+
rawFile &&
|
|
3133
|
+
typeof rawFile === "object" &&
|
|
3134
|
+
!Array.isArray(rawFile) &&
|
|
3135
|
+
typeof rawFile.path === "string" &&
|
|
3136
|
+
typeof rawFile.mimeType === "string"
|
|
3137
|
+
) {
|
|
3138
|
+
artFile = {
|
|
3139
|
+
path: rawFile.path,
|
|
3140
|
+
mimeType: rawFile.mimeType,
|
|
3141
|
+
...(typeof rawFile.language === "string" ? { language: rawFile.language } : {}),
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3108
3144
|
onEvent({
|
|
3109
3145
|
type: "artifact_start",
|
|
3110
3146
|
id: artId,
|
|
3111
3147
|
artifactType: at,
|
|
3112
3148
|
title: artTitle,
|
|
3113
|
-
component: typeof payload.component === "string" ? payload.component : undefined
|
|
3149
|
+
component: typeof payload.component === "string" ? payload.component : undefined,
|
|
3150
|
+
...(artFile ? { file: artFile } : {})
|
|
3114
3151
|
});
|
|
3115
|
-
|
|
3116
|
-
//
|
|
3152
|
+
// Seed component props from artifact_start when the payload
|
|
3153
|
+
// carries them; artifact_update events accumulate the rest below.
|
|
3154
|
+
const startProps =
|
|
3155
|
+
payload.props && typeof payload.props === "object" && !Array.isArray(payload.props)
|
|
3156
|
+
? { ...(payload.props as Record<string, unknown>) }
|
|
3157
|
+
: undefined;
|
|
3158
|
+
artifactContent.set(artId, {
|
|
3159
|
+
markdown: "",
|
|
3160
|
+
title: artTitle,
|
|
3161
|
+
file: artFile,
|
|
3162
|
+
...(startProps ? { props: startProps } : {})
|
|
3163
|
+
});
|
|
3164
|
+
// Insert the in-thread artifact block (skip if already present from
|
|
3165
|
+
// transcript_insert). The resolved display mode picks the component:
|
|
3166
|
+
// "card"/"panel" inject the reference card; "inline" injects the
|
|
3167
|
+
// inline preview block. Both share the rawContent JSON-component
|
|
3168
|
+
// shape so transcript persistence and hydration work unchanged.
|
|
3117
3169
|
if (!artifactIdsWithCards.has(artId)) {
|
|
3118
3170
|
artifactIdsWithCards.add(artId);
|
|
3171
|
+
const displayMode = resolveArtifactDisplayMode(
|
|
3172
|
+
this.config.features?.artifacts,
|
|
3173
|
+
at
|
|
3174
|
+
);
|
|
3175
|
+
const artComponent =
|
|
3176
|
+
typeof payload.component === "string" ? payload.component : undefined;
|
|
3119
3177
|
const cardMsg: AgentWidgetMessage = {
|
|
3120
3178
|
id: `artifact-ref-${artId}`,
|
|
3121
3179
|
role: "assistant",
|
|
@@ -3123,9 +3181,13 @@ export class AgentWidgetClient {
|
|
|
3123
3181
|
createdAt: new Date().toISOString(),
|
|
3124
3182
|
streaming: true,
|
|
3125
3183
|
sequence: nextSequence(),
|
|
3126
|
-
rawContent:
|
|
3127
|
-
|
|
3128
|
-
|
|
3184
|
+
rawContent: buildArtifactRefRawContent(displayMode, {
|
|
3185
|
+
artifactId: artId,
|
|
3186
|
+
title: artTitle,
|
|
3187
|
+
artifactType: at,
|
|
3188
|
+
status: "streaming",
|
|
3189
|
+
...(artFile ? { file: artFile } : {}),
|
|
3190
|
+
...(artComponent ? { component: artComponent } : {}),
|
|
3129
3191
|
}),
|
|
3130
3192
|
};
|
|
3131
3193
|
artifactCardMessages.set(artId, cardMsg);
|
|
@@ -3152,6 +3214,13 @@ export class AgentWidgetClient {
|
|
|
3152
3214
|
props,
|
|
3153
3215
|
component: typeof payload.component === "string" ? payload.component : undefined
|
|
3154
3216
|
});
|
|
3217
|
+
// Accumulate for hydration: embedded on artifact_complete so an
|
|
3218
|
+
// inline component block re-renders with its real props after a
|
|
3219
|
+
// refresh.
|
|
3220
|
+
const updateAcc = artifactContent.get(String(payload.id));
|
|
3221
|
+
if (updateAcc) {
|
|
3222
|
+
updateAcc.props = { ...(updateAcc.props ?? {}), ...props };
|
|
3223
|
+
}
|
|
3155
3224
|
} else if (payloadType === "artifact_complete") {
|
|
3156
3225
|
const artCompleteId = String(payload.id);
|
|
3157
3226
|
onEvent({ type: "artifact_complete", id: artCompleteId });
|
|
@@ -3168,6 +3237,20 @@ export class AgentWidgetClient {
|
|
|
3168
3237
|
if (acc?.markdown) {
|
|
3169
3238
|
parsed.props.markdown = acc.markdown;
|
|
3170
3239
|
}
|
|
3240
|
+
// Persist file metadata too so the download path unfences correctly after refresh.
|
|
3241
|
+
if (acc?.file) {
|
|
3242
|
+
parsed.props.file = acc.file;
|
|
3243
|
+
}
|
|
3244
|
+
// Embed accumulated component props so an inline component
|
|
3245
|
+
// block hydrates with its real props after a refresh. Only
|
|
3246
|
+
// the inline block reads them; the card never does.
|
|
3247
|
+
if (
|
|
3248
|
+
parsed.component === "PersonaArtifactInline" &&
|
|
3249
|
+
acc?.props &&
|
|
3250
|
+
Object.keys(acc.props).length > 0
|
|
3251
|
+
) {
|
|
3252
|
+
parsed.props.componentProps = acc.props;
|
|
3253
|
+
}
|
|
3171
3254
|
}
|
|
3172
3255
|
refMsg.rawContent = JSON.stringify(parsed);
|
|
3173
3256
|
} catch { /* ignore parse errors */ }
|