@runtypelabs/persona 4.7.0 → 4.9.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 (185) hide show
  1. package/README.md +1 -0
  2. package/dist/animations/glyph-cycle.cjs +2 -2
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/glyph-cycle.js +2 -2
  6. package/dist/animations/{types-CSmiKRVa.d.cts → types-4ROVJ1gA.d.cts} +85 -3
  7. package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -3
  8. package/dist/animations/wipe.cjs +2 -2
  9. package/dist/animations/wipe.d.cts +1 -1
  10. package/dist/animations/wipe.d.ts +1 -1
  11. package/dist/chunk-5EIIHQLQ.js +1 -0
  12. package/dist/chunk-IO5VVUKP.js +3 -0
  13. package/dist/chunk-IPVK3KOM.js +1 -0
  14. package/dist/chunk-UPO4GUFC.js +1 -0
  15. package/dist/codegen.cjs +13 -13
  16. package/dist/codegen.js +14 -14
  17. package/dist/context-mentions-7S5KVUTG.js +169 -0
  18. package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
  19. package/dist/context-mentions-inline.cjs +4 -0
  20. package/dist/context-mentions-inline.d.cts +203 -0
  21. package/dist/context-mentions-inline.d.ts +203 -0
  22. package/dist/context-mentions-inline.js +4 -0
  23. package/dist/context-mentions.cjs +295 -0
  24. package/dist/context-mentions.d.cts +7025 -0
  25. package/dist/context-mentions.d.ts +7025 -0
  26. package/dist/context-mentions.js +295 -0
  27. package/dist/index.cjs +99 -68
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +1507 -182
  30. package/dist/index.d.ts +1507 -182
  31. package/dist/index.global.js +88 -56
  32. package/dist/index.global.js.map +1 -1
  33. package/dist/index.js +99 -68
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.global.js +1 -1
  36. package/dist/install.global.js.map +1 -1
  37. package/dist/launcher.global.js +3 -2
  38. package/dist/launcher.global.js.map +1 -1
  39. package/dist/markdown-parsers.js +24 -24
  40. package/dist/plugin-kit.cjs +1 -1
  41. package/dist/plugin-kit.d.cts +17 -0
  42. package/dist/plugin-kit.d.ts +17 -0
  43. package/dist/plugin-kit.js +1 -1
  44. package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
  45. package/dist/runtype-tts.js +1 -1
  46. package/dist/session-reconnect-JKIJBHS5.js +1 -0
  47. package/dist/smart-dom-reader.cjs +18 -17
  48. package/dist/smart-dom-reader.d.cts +1260 -16
  49. package/dist/smart-dom-reader.d.ts +1260 -16
  50. package/dist/smart-dom-reader.js +18 -17
  51. package/dist/testing.cjs +3 -3
  52. package/dist/testing.js +3 -3
  53. package/dist/theme-editor-preview.cjs +260 -58
  54. package/dist/theme-editor-preview.d.cts +1234 -15
  55. package/dist/theme-editor-preview.d.ts +1234 -15
  56. package/dist/theme-editor-preview.js +87 -58
  57. package/dist/theme-editor.cjs +10 -10
  58. package/dist/theme-editor.d.cts +1226 -15
  59. package/dist/theme-editor.d.ts +1226 -15
  60. package/dist/theme-editor.js +10 -10
  61. package/dist/theme-reference.cjs +1 -1
  62. package/dist/theme-reference.d.cts +76 -0
  63. package/dist/theme-reference.d.ts +76 -0
  64. package/dist/theme-reference.js +1 -1
  65. package/dist/voice-worklet-player.cjs +2 -2
  66. package/dist/voice-worklet-player.js +2 -2
  67. package/dist/webmcp-polyfill.js +2 -2
  68. package/dist/widget.css +1 -1
  69. package/package.json +16 -5
  70. package/src/artifacts-session.test.ts +178 -0
  71. package/src/client.test.ts +255 -1
  72. package/src/client.ts +157 -60
  73. package/src/components/artifact-card.test.ts +333 -0
  74. package/src/components/artifact-card.ts +75 -28
  75. package/src/components/artifact-inline.test.ts +1328 -0
  76. package/src/components/artifact-inline.ts +920 -0
  77. package/src/components/artifact-pane.test.ts +1042 -0
  78. package/src/components/artifact-pane.ts +440 -131
  79. package/src/components/artifact-preview.test.ts +1155 -0
  80. package/src/components/artifact-preview.ts +994 -0
  81. package/src/components/composer-parts.ts +3 -12
  82. package/src/components/context-mention-button.test.ts +70 -0
  83. package/src/components/context-mention-button.ts +82 -0
  84. package/src/components/context-mention-chip.ts +134 -0
  85. package/src/components/context-mention-menu.test.ts +508 -0
  86. package/src/components/context-mention-menu.ts +0 -0
  87. package/src/components/message-bubble.test.ts +175 -0
  88. package/src/components/message-bubble.ts +177 -19
  89. package/src/components/panel.ts +7 -10
  90. package/src/components/pill-composer-builder.test.ts +6 -2
  91. package/src/components/pill-composer-builder.ts +6 -6
  92. package/src/components/reasoning-bubble.ts +1 -13
  93. package/src/components/registry.ts +38 -3
  94. package/src/components/tool-bubble.ts +1 -13
  95. package/src/context-mentions-bundle.test.ts +163 -0
  96. package/src/context-mentions-entry.ts +185 -0
  97. package/src/context-mentions-inline-entry.test.ts +136 -0
  98. package/src/context-mentions-inline-entry.ts +226 -0
  99. package/src/context-mentions-inline-loader.test.ts +30 -0
  100. package/src/context-mentions-inline-loader.ts +36 -0
  101. package/src/context-mentions-inline.ts +15 -0
  102. package/src/context-mentions-loader.ts +32 -0
  103. package/src/context-mentions.ts +16 -0
  104. package/src/defaults.ts +1 -0
  105. package/src/generated/runtype-openapi-contract.ts +55 -3
  106. package/src/index-core.ts +46 -1
  107. package/src/index-global.ts +51 -0
  108. package/src/index.ts +8 -0
  109. package/src/markdown-parsers-loader.test.ts +158 -0
  110. package/src/markdown-parsers-loader.ts +83 -27
  111. package/src/plugin-kit.test.ts +40 -0
  112. package/src/plugin-kit.ts +39 -5
  113. package/src/runtime/host-layout.test.ts +163 -0
  114. package/src/runtime/host-layout.ts +110 -7
  115. package/src/runtime/init.ts +18 -61
  116. package/src/session.mentions.test.ts +175 -0
  117. package/src/session.test.ts +52 -4
  118. package/src/session.ts +197 -27
  119. package/src/smart-dom-reader.test.ts +129 -2
  120. package/src/smart-dom-reader.ts +127 -1
  121. package/src/styles/context-mention-menu-css.ts +176 -0
  122. package/src/styles/widget.css +1002 -152
  123. package/src/theme-editor/preview.ts +2 -0
  124. package/src/theme-editor/sections.test.ts +26 -1
  125. package/src/theme-editor/sections.ts +13 -5
  126. package/src/theme-reference.ts +2 -2
  127. package/src/tool-call-display-defaults.test.ts +1 -0
  128. package/src/types/theme.ts +79 -0
  129. package/src/types.ts +1041 -17
  130. package/src/ui.artifact-pane-gating.test.ts +636 -0
  131. package/src/ui.component-directive.test.ts +104 -0
  132. package/src/ui.composer-bar.test.ts +60 -2
  133. package/src/ui.detached-panel.test.ts +1049 -0
  134. package/src/ui.mention-submit.test.ts +235 -0
  135. package/src/ui.tool-display.test.ts +51 -0
  136. package/src/ui.ts +998 -159
  137. package/src/utils/artifact-custom-actions.ts +128 -0
  138. package/src/utils/artifact-display.test.ts +42 -0
  139. package/src/utils/artifact-display.ts +84 -0
  140. package/src/utils/artifact-file.test.ts +116 -0
  141. package/src/utils/artifact-file.ts +117 -0
  142. package/src/utils/artifact-gate.test.ts +112 -5
  143. package/src/utils/artifact-gate.ts +39 -14
  144. package/src/utils/artifact-loading-status.ts +55 -0
  145. package/src/utils/artifact-status-label.ts +190 -0
  146. package/src/utils/buttons.ts +7 -1
  147. package/src/utils/chunk-loader.test.ts +97 -0
  148. package/src/utils/chunk-loader.ts +88 -0
  149. package/src/utils/code-highlight.test.ts +186 -0
  150. package/src/utils/code-highlight.ts +400 -0
  151. package/src/utils/composer-contenteditable.test.ts +507 -0
  152. package/src/utils/composer-contenteditable.ts +626 -0
  153. package/src/utils/composer-document.test.ts +280 -0
  154. package/src/utils/composer-document.ts +293 -0
  155. package/src/utils/composer-history.test.ts +35 -7
  156. package/src/utils/composer-history.ts +30 -20
  157. package/src/utils/composer-input.ts +159 -0
  158. package/src/utils/context-mention-controller.test.ts +1215 -0
  159. package/src/utils/context-mention-controller.ts +1186 -0
  160. package/src/utils/context-mention-manager.test.ts +422 -0
  161. package/src/utils/context-mention-manager.ts +410 -0
  162. package/src/utils/context-mention-orchestrator.test.ts +538 -0
  163. package/src/utils/context-mention-orchestrator.ts +348 -0
  164. package/src/utils/icons.ts +2 -0
  165. package/src/utils/live-region.test.ts +108 -0
  166. package/src/utils/live-region.ts +94 -0
  167. package/src/utils/mention-channels.ts +63 -0
  168. package/src/utils/mention-llm-format.test.ts +91 -0
  169. package/src/utils/mention-llm-format.ts +79 -0
  170. package/src/utils/mention-matcher.test.ts +86 -0
  171. package/src/utils/mention-matcher.ts +221 -0
  172. package/src/utils/mention-token.ts +72 -0
  173. package/src/utils/mention-trigger.test.ts +155 -0
  174. package/src/utils/mention-trigger.ts +156 -0
  175. package/src/utils/roving-tablist.test.ts +152 -0
  176. package/src/utils/roving-tablist.ts +111 -0
  177. package/src/utils/spinner.ts +45 -0
  178. package/src/utils/theme.test.ts +76 -4
  179. package/src/utils/theme.ts +7 -0
  180. package/src/utils/tokens.ts +118 -11
  181. package/src/utils/tool-loading-animation.test.ts +32 -0
  182. package/src/utils/tool-loading-animation.ts +24 -0
  183. package/dist/chunk-DFBSCFYN.js +0 -1
  184. package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
  185. 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.9.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",
@@ -62,6 +62,16 @@
62
62
  "import": "./dist/animations/wipe.js",
63
63
  "require": "./dist/animations/wipe.cjs"
64
64
  },
65
+ "./context-mentions": {
66
+ "types": "./dist/context-mentions.d.ts",
67
+ "import": "./dist/context-mentions.js",
68
+ "require": "./dist/context-mentions.cjs"
69
+ },
70
+ "./context-mentions-inline": {
71
+ "types": "./dist/context-mentions-inline.d.ts",
72
+ "import": "./dist/context-mentions-inline.js",
73
+ "require": "./dist/context-mentions-inline.cjs"
74
+ },
65
75
  "./widget.css": "./dist/widget.css"
66
76
  },
67
77
  "files": [
@@ -74,8 +84,7 @@
74
84
  "idiomorph": "^0.7.4",
75
85
  "lucide": "^1.18.0",
76
86
  "marked": "^12.0.2",
77
- "partial-json": "^0.1.7",
78
- "zod": "^3.22.4"
87
+ "partial-json": "^0.1.7"
79
88
  },
80
89
  "devDependencies": {
81
90
  "@size-limit/file": "^12.1.0",
@@ -121,7 +130,7 @@
121
130
  "access": "public"
122
131
  },
123
132
  "scripts": {
124
- "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations && pnpm run check:dist-vite8",
133
+ "build": "rimraf dist && pnpm run build:styles && pnpm run build:markdown-parsers && pnpm run build:client && pnpm run build:installer && pnpm run build:launcher && pnpm run build:webmcp-polyfill && pnpm run build:context-mentions && pnpm run build:context-mentions-inline && pnpm run build:runtype-tts && pnpm run build:theme-ref && pnpm run build:codegen && pnpm run build:theme-editor && pnpm run build:theme-editor-preview && pnpm run build:testing && pnpm run build:smart-dom-reader && pnpm run build:voice-worklet-player && pnpm run build:plugin-kit && pnpm run build:animations && pnpm run check:dist-vite8",
125
134
  "check:dist-vite8": "node scripts/check-dist-no-in-for-init.mjs",
126
135
  "build:markdown-parsers": "tsup --config tsup.markdown-parsers.config.ts",
127
136
  "build:plugin-kit": "tsup src/plugin-kit.ts --format esm,cjs --minify --dts --out-dir dist --no-splitting",
@@ -134,8 +143,10 @@
134
143
  "build:theme-ref": "tsup src/theme-reference.ts --format esm,cjs --minify --dts",
135
144
  "build:codegen": "tsup src/codegen.ts --format esm,cjs --minify --dts",
136
145
  "build:styles": "node scripts/build-styles.mjs",
137
- "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" && tsup --config tsup.global.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
146
+ "build:client": "tsup src/index.ts --format esm,cjs --minify --sourcemap --splitting false --dts --loader \".css=text\" --external @runtypelabs/persona/context-mentions --external @runtypelabs/persona/context-mentions-inline && tsup --config tsup.global.config.ts && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/index-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/index'+ext);}\"",
138
147
  "build:webmcp-polyfill": "tsup --config tsup.webmcp-polyfill.config.ts",
148
+ "build:context-mentions": "tsup --config tsup.context-mentions.config.ts",
149
+ "build:context-mentions-inline": "tsup --config tsup.context-mentions-inline.config.ts",
139
150
  "build:runtype-tts": "tsup --config tsup.runtype-tts.config.ts",
140
151
  "build:installer": "tsup src/install.ts --format iife --global-name SiteAgentInstaller --out-dir dist --minify --sourcemap --no-splitting",
141
152
  "build:launcher": "tsup src/launcher-global.ts --format iife --global-name AgentWidgetLauncher --minify --sourcemap --splitting false --out-dir dist && node -e \"const fs=require('fs');for(const ext of ['.global.js','.global.js.map']){const from='dist/launcher-global'+ext;if(fs.existsSync(from))fs.renameSync(from,'dist/launcher'+ext);}\"",
@@ -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,257 @@ 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
+ });
4545
+
4546
+ describe('AgentWidgetClient - mention context', () => {
4547
+ const agentDone = (controller: ReadableStreamDefaultController, encoder: TextEncoder) => {
4548
+ controller.enqueue(encoder.encode(sseEvent('agent_complete', {
4549
+ executionId: 'exec_1',
4550
+ agentId: 'agent_123',
4551
+ success: true,
4552
+ iterations: 1,
4553
+ completedAt: new Date().toISOString(),
4554
+ seq: 1,
4555
+ })));
4556
+ controller.close();
4557
+ };
4558
+
4559
+ const captureContext = () => {
4560
+ let capturedPayload: any = null;
4561
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
4562
+ capturedPayload = JSON.parse(options.body);
4563
+ const encoder = new TextEncoder();
4564
+ return {
4565
+ ok: true,
4566
+ body: new ReadableStream({ start(c) { agentDone(c, encoder); } }),
4567
+ };
4568
+ });
4569
+ return () => capturedPayload;
4570
+ };
4571
+
4572
+ it('attaches the latest user turn mention context under context.mentions', async () => {
4573
+ const get = captureContext();
4574
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000', agentId: 'agent_123' });
4575
+
4576
+ await client.dispatch({
4577
+ messages: [{
4578
+ id: 'u1', role: 'user', content: 'Summarize this',
4579
+ createdAt: '2025-01-01T00:00:00.000Z',
4580
+ mentionContext: { files: { app: { path: 'App.tsx' } } },
4581
+ } as AgentWidgetMessage],
4582
+ }, () => {});
4583
+
4584
+ expect(get().context).toEqual({ mentions: { files: { app: { path: 'App.tsx' } } } });
4585
+ });
4586
+
4587
+ it('does NOT re-attach an older turn mention context on a later send', async () => {
4588
+ const get = captureContext();
4589
+ const client = new AgentWidgetClient({ apiUrl: 'http://localhost:8000', agentId: 'agent_123' });
4590
+
4591
+ await client.dispatch({
4592
+ messages: [
4593
+ {
4594
+ id: 'u1', role: 'user', content: 'Summarize this',
4595
+ createdAt: '2025-01-01T00:00:00.000Z',
4596
+ mentionContext: { files: { app: { path: 'App.tsx' } } },
4597
+ } as AgentWidgetMessage,
4598
+ {
4599
+ id: 'a1', role: 'assistant', content: 'Done',
4600
+ createdAt: '2025-01-01T00:00:01.000Z',
4601
+ } as AgentWidgetMessage,
4602
+ {
4603
+ id: 'u2', role: 'user', content: 'Now a plain follow-up',
4604
+ createdAt: '2025-01-01T00:00:02.000Z',
4605
+ } as AgentWidgetMessage,
4606
+ ],
4607
+ }, () => {});
4608
+
4609
+ // The most recent user turn (u2) carried no mentionContext, so the stale
4610
+ // context from u1 must not leak into this request.
4611
+ expect(get().context).toBeUndefined();
4612
+ });
4613
+ });