@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.
Files changed (117) hide show
  1. package/dist/animations/glyph-cycle.cjs +2 -2
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/glyph-cycle.js +2 -2
  5. package/dist/animations/{types-CSmiKRVa.d.cts → types-BsZtXPKK.d.cts} +43 -3
  6. package/dist/animations/{types-CSmiKRVa.d.ts → types-BsZtXPKK.d.ts} +43 -3
  7. package/dist/animations/wipe.cjs +2 -2
  8. package/dist/animations/wipe.d.cts +1 -1
  9. package/dist/animations/wipe.d.ts +1 -1
  10. package/dist/chunk-5EIIHQLQ.js +1 -0
  11. package/dist/codegen.cjs +12 -12
  12. package/dist/codegen.js +14 -14
  13. package/dist/index.cjs +91 -68
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +877 -172
  16. package/dist/index.d.ts +877 -172
  17. package/dist/index.global.js +81 -57
  18. package/dist/index.global.js.map +1 -1
  19. package/dist/index.js +91 -68
  20. package/dist/index.js.map +1 -1
  21. package/dist/install.global.js +1 -1
  22. package/dist/install.global.js.map +1 -1
  23. package/dist/launcher.global.js +3 -2
  24. package/dist/launcher.global.js.map +1 -1
  25. package/dist/markdown-parsers.js +24 -24
  26. package/dist/plugin-kit.cjs +1 -1
  27. package/dist/plugin-kit.js +1 -1
  28. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  29. package/dist/runtype-tts.js +1 -1
  30. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  31. package/dist/smart-dom-reader.cjs +17 -17
  32. package/dist/smart-dom-reader.d.cts +753 -15
  33. package/dist/smart-dom-reader.d.ts +753 -15
  34. package/dist/smart-dom-reader.js +17 -17
  35. package/dist/testing.cjs +3 -3
  36. package/dist/testing.js +3 -3
  37. package/dist/theme-editor-preview.cjs +81 -58
  38. package/dist/theme-editor-preview.d.cts +761 -15
  39. package/dist/theme-editor-preview.d.ts +761 -15
  40. package/dist/theme-editor-preview.js +81 -58
  41. package/dist/theme-editor.cjs +6 -6
  42. package/dist/theme-editor.d.cts +753 -15
  43. package/dist/theme-editor.d.ts +753 -15
  44. package/dist/theme-editor.js +10 -10
  45. package/dist/theme-reference.cjs +1 -1
  46. package/dist/theme-reference.d.cts +74 -0
  47. package/dist/theme-reference.d.ts +74 -0
  48. package/dist/theme-reference.js +1 -1
  49. package/dist/voice-worklet-player.cjs +2 -2
  50. package/dist/voice-worklet-player.js +2 -2
  51. package/dist/webmcp-polyfill.js +2 -2
  52. package/dist/widget.css +1 -1
  53. package/package.json +2 -3
  54. package/src/artifacts-session.test.ts +178 -0
  55. package/src/client.test.ts +186 -1
  56. package/src/client.ts +92 -9
  57. package/src/components/artifact-card.test.ts +333 -0
  58. package/src/components/artifact-card.ts +75 -28
  59. package/src/components/artifact-inline.test.ts +1328 -0
  60. package/src/components/artifact-inline.ts +920 -0
  61. package/src/components/artifact-pane.test.ts +1042 -0
  62. package/src/components/artifact-pane.ts +440 -131
  63. package/src/components/artifact-preview.test.ts +1155 -0
  64. package/src/components/artifact-preview.ts +994 -0
  65. package/src/components/pill-composer-builder.test.ts +6 -2
  66. package/src/components/pill-composer-builder.ts +6 -6
  67. package/src/components/reasoning-bubble.ts +1 -13
  68. package/src/components/registry.ts +38 -3
  69. package/src/components/tool-bubble.ts +1 -13
  70. package/src/defaults.ts +1 -0
  71. package/src/generated/runtype-openapi-contract.ts +55 -3
  72. package/src/index-core.ts +20 -1
  73. package/src/index.ts +8 -0
  74. package/src/markdown-parsers-loader.test.ts +158 -0
  75. package/src/markdown-parsers-loader.ts +74 -9
  76. package/src/runtime/host-layout.test.ts +163 -0
  77. package/src/runtime/host-layout.ts +110 -7
  78. package/src/runtime/init.ts +18 -61
  79. package/src/session.ts +76 -22
  80. package/src/styles/widget.css +773 -26
  81. package/src/theme-editor/preview.ts +2 -0
  82. package/src/theme-editor/sections.test.ts +26 -1
  83. package/src/theme-editor/sections.ts +10 -2
  84. package/src/theme-reference.ts +2 -2
  85. package/src/tool-call-display-defaults.test.ts +1 -0
  86. package/src/types/theme.ts +77 -0
  87. package/src/types.ts +516 -17
  88. package/src/ui.artifact-pane-gating.test.ts +636 -0
  89. package/src/ui.component-directive.test.ts +104 -0
  90. package/src/ui.composer-bar.test.ts +60 -2
  91. package/src/ui.detached-panel.test.ts +1049 -0
  92. package/src/ui.tool-display.test.ts +51 -0
  93. package/src/ui.ts +698 -109
  94. package/src/utils/artifact-custom-actions.ts +128 -0
  95. package/src/utils/artifact-display.test.ts +42 -0
  96. package/src/utils/artifact-display.ts +84 -0
  97. package/src/utils/artifact-file.test.ts +116 -0
  98. package/src/utils/artifact-file.ts +117 -0
  99. package/src/utils/artifact-gate.test.ts +112 -5
  100. package/src/utils/artifact-gate.ts +39 -14
  101. package/src/utils/artifact-loading-status.ts +55 -0
  102. package/src/utils/artifact-status-label.ts +190 -0
  103. package/src/utils/buttons.ts +7 -1
  104. package/src/utils/code-highlight.test.ts +186 -0
  105. package/src/utils/code-highlight.ts +400 -0
  106. package/src/utils/icons.ts +2 -0
  107. package/src/utils/roving-tablist.test.ts +152 -0
  108. package/src/utils/roving-tablist.ts +111 -0
  109. package/src/utils/spinner.ts +45 -0
  110. package/src/utils/theme.test.ts +48 -0
  111. package/src/utils/theme.ts +7 -0
  112. package/src/utils/tokens.ts +91 -0
  113. package/src/utils/tool-loading-animation.test.ts +32 -0
  114. package/src/utils/tool-loading-animation.ts +24 -0
  115. package/dist/chunk-DFBSCFYN.js +0 -1
  116. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  117. 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.7.0",
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
  });
@@ -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 messages so we can update them on artifact_complete
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 for embedding in card props on complete
1709
- const artifactContent = new Map<string, { markdown: string; title?: string }>();
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
- artifactContent.set(artId, { markdown: "", title: artTitle });
3116
- // Insert inline artifact reference card (skip if already present from transcript_insert)
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: JSON.stringify({
3127
- component: "PersonaArtifactCard",
3128
- props: { artifactId: artId, title: artTitle, artifactType: at, status: "streaming" },
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 */ }