@runtypelabs/persona 3.19.0 → 3.20.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.
@@ -7505,7 +7505,8 @@ var AgentWidgetSession = class _AgentWidgetSession {
7505
7505
  createdAt,
7506
7506
  sequence,
7507
7507
  streaming = false,
7508
- voiceProcessing
7508
+ voiceProcessing,
7509
+ rawContent
7509
7510
  } = options;
7510
7511
  const messageId = id != null ? id : role === "user" ? generateUserMessageId() : role === "assistant" ? generateAssistantMessageId() : `system-${Date.now()}-${Math.random().toString(16).slice(2)}`;
7511
7512
  const message = {
@@ -7518,7 +7519,8 @@ var AgentWidgetSession = class _AgentWidgetSession {
7518
7519
  // Only include optional fields if provided
7519
7520
  ...llmContent !== void 0 && { llmContent },
7520
7521
  ...contentParts !== void 0 && { contentParts },
7521
- ...voiceProcessing !== void 0 && { voiceProcessing }
7522
+ ...voiceProcessing !== void 0 && { voiceProcessing },
7523
+ ...rawContent !== void 0 && { rawContent }
7522
7524
  };
7523
7525
  this.upsertMessage(message);
7524
7526
  return message;
@@ -7583,7 +7585,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7583
7585
  id,
7584
7586
  createdAt,
7585
7587
  sequence,
7586
- streaming = false
7588
+ streaming = false,
7589
+ voiceProcessing,
7590
+ rawContent
7587
7591
  } = options;
7588
7592
  const messageId = id != null ? id : role === "user" ? generateUserMessageId() : role === "assistant" ? generateAssistantMessageId() : `system-${Date.now()}-${Math.random().toString(16).slice(2)}`;
7589
7593
  const message = {
@@ -7594,7 +7598,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7594
7598
  sequence: sequence != null ? sequence : this.nextSequence(),
7595
7599
  streaming,
7596
7600
  ...llmContent !== void 0 && { llmContent },
7597
- ...contentParts !== void 0 && { contentParts }
7601
+ ...contentParts !== void 0 && { contentParts },
7602
+ ...voiceProcessing !== void 0 && { voiceProcessing },
7603
+ ...rawContent !== void 0 && { rawContent }
7598
7604
  };
7599
7605
  results.push(message);
7600
7606
  }
@@ -7602,6 +7608,48 @@ var AgentWidgetSession = class _AgentWidgetSession {
7602
7608
  this.callbacks.onMessagesChanged([...this.messages]);
7603
7609
  return results;
7604
7610
  }
7611
+ /**
7612
+ * Convenience method for injecting a registered component directive as
7613
+ * an assistant message — the same shape Persona produces from a streamed
7614
+ * `{ "text": "...", "component": "...", "props": {...} }` payload.
7615
+ *
7616
+ * Sets `content` to `text`, `rawContent` to the JSON directive (so
7617
+ * `extractComponentDirectiveFromMessage` can find it), and forwards
7618
+ * `llmContent` / `id` / `createdAt` / `sequence`.
7619
+ *
7620
+ * @example
7621
+ * session.injectComponentDirective({
7622
+ * component: "DynamicForm",
7623
+ * props: { title: "Book a demo", fields: [...] },
7624
+ * text: "Share your details to book a demo.",
7625
+ * llmContent: "[Showed booking form]"
7626
+ * });
7627
+ */
7628
+ injectComponentDirective(options) {
7629
+ const {
7630
+ component,
7631
+ props = {},
7632
+ text = "",
7633
+ llmContent,
7634
+ id,
7635
+ createdAt,
7636
+ sequence
7637
+ } = options;
7638
+ const directive = {
7639
+ text,
7640
+ component,
7641
+ props
7642
+ };
7643
+ return this.injectMessage({
7644
+ role: "assistant",
7645
+ content: text,
7646
+ rawContent: JSON.stringify(directive),
7647
+ ...llmContent !== void 0 && { llmContent },
7648
+ ...id !== void 0 && { id },
7649
+ ...createdAt !== void 0 && { createdAt },
7650
+ ...sequence !== void 0 && { sequence }
7651
+ });
7652
+ }
7605
7653
  async sendMessage(rawInput, options) {
7606
7654
  var _a, _b, _c, _d, _e;
7607
7655
  const input = rawInput.trim();
@@ -15326,24 +15374,39 @@ function renderComponentDirective(directive, options) {
15326
15374
  return null;
15327
15375
  }
15328
15376
  }
15377
+ function selectDirectiveSource(message) {
15378
+ if (typeof message.rawContent === "string" && message.rawContent.length > 0) {
15379
+ return message.rawContent;
15380
+ }
15381
+ if (typeof message.content === "string") {
15382
+ const trimmed = message.content.trim();
15383
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15384
+ return message.content;
15385
+ }
15386
+ }
15387
+ return null;
15388
+ }
15329
15389
  function hasComponentDirective(message) {
15330
- if (!message.rawContent) return false;
15390
+ const source = selectDirectiveSource(message);
15391
+ if (!source) return false;
15331
15392
  try {
15332
- const parsed = JSON.parse(message.rawContent);
15393
+ const parsed = JSON.parse(source);
15333
15394
  return typeof parsed === "object" && parsed !== null && "component" in parsed && typeof parsed.component === "string";
15334
15395
  } catch {
15335
15396
  return false;
15336
15397
  }
15337
15398
  }
15338
15399
  function extractComponentDirectiveFromMessage(message) {
15339
- if (!message.rawContent) return null;
15400
+ const source = selectDirectiveSource(message);
15401
+ if (!source) return null;
15340
15402
  try {
15341
- const parsed = JSON.parse(message.rawContent);
15403
+ const parsed = JSON.parse(source);
15342
15404
  if (typeof parsed === "object" && parsed !== null && "component" in parsed && typeof parsed.component === "string") {
15405
+ const directive = parsed;
15343
15406
  return {
15344
- component: parsed.component,
15345
- props: parsed.props && typeof parsed.props === "object" && parsed.props !== null ? parsed.props : {},
15346
- raw: message.rawContent
15407
+ component: directive.component,
15408
+ props: directive.props && typeof directive.props === "object" && directive.props !== null ? directive.props : {},
15409
+ raw: source
15347
15410
  };
15348
15411
  }
15349
15412
  } catch {
@@ -20743,6 +20806,12 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
20743
20806
  }
20744
20807
  return session.injectMessageBatch(optionsList);
20745
20808
  },
20809
+ injectComponentDirective(options) {
20810
+ if (!open && isPanelToggleable()) {
20811
+ setOpenState(true, "system");
20812
+ }
20813
+ return session.injectComponentDirective(options);
20814
+ },
20746
20815
  /** @deprecated Use injectMessage() instead */
20747
20816
  injectTestMessage(event) {
20748
20817
  if (!open && isPanelToggleable()) {
@@ -4097,6 +4097,22 @@ type InjectMessageOptions = {
4097
4097
  * Consumers can detect this in `messageTransform` to render custom UI.
4098
4098
  */
4099
4099
  voiceProcessing?: boolean;
4100
+ /**
4101
+ * Raw structured payload (typically a JSON string) representing the
4102
+ * full directive that produced this message — e.g. `{ "text": "...",
4103
+ * "component": "Foo", "props": {...} }`.
4104
+ *
4105
+ * Mirrors the field populated by stream parsers during normal LLM
4106
+ * responses. Set this when injecting a message that should render as a
4107
+ * component directive (`hasComponentDirective` /
4108
+ * `extractComponentDirectiveFromMessage` look at `rawContent` first).
4109
+ *
4110
+ * Priority for the API payload remains:
4111
+ * `contentParts > llmContent > rawContent > content`. Pass `llmContent`
4112
+ * alongside `rawContent` if the LLM should see something other than the
4113
+ * raw directive.
4114
+ */
4115
+ rawContent?: string;
4100
4116
  };
4101
4117
  /**
4102
4118
  * Options for injecting assistant messages (most common case).
@@ -4113,6 +4129,57 @@ type InjectUserMessageOptions = Omit<InjectMessageOptions, "role">;
4113
4129
  * Role defaults to 'system'.
4114
4130
  */
4115
4131
  type InjectSystemMessageOptions = Omit<InjectMessageOptions, "role">;
4132
+ /**
4133
+ * Options for injecting an assistant message that renders as a component
4134
+ * directive — sugar over `injectAssistantMessage` for the common case of
4135
+ * "render this registered component, same as if the LLM had emitted it".
4136
+ *
4137
+ * Equivalent to calling `injectAssistantMessage({ content: text, rawContent:
4138
+ * JSON.stringify({ text, component, props }), llmContent })`.
4139
+ *
4140
+ * @example
4141
+ * widget.injectComponentDirective({
4142
+ * component: "DynamicForm",
4143
+ * props: { title: "Book a demo", fields: [...] },
4144
+ * text: "Share your details to book a demo.",
4145
+ * llmContent: "[Showed booking form]"
4146
+ * });
4147
+ */
4148
+ type InjectComponentDirectiveOptions = {
4149
+ /**
4150
+ * Name of a renderer registered via `componentRegistry.register(...)`.
4151
+ */
4152
+ component: string;
4153
+ /**
4154
+ * Props passed to the component renderer.
4155
+ */
4156
+ props?: Record<string, unknown>;
4157
+ /**
4158
+ * Bubble copy displayed above (or with) the rendered component.
4159
+ * Mirrors the `text` field in a streamed JSON directive.
4160
+ * @default ""
4161
+ */
4162
+ text?: string;
4163
+ /**
4164
+ * Content sent to the LLM in API requests. When omitted, the raw
4165
+ * directive JSON is what the LLM would see (per the standard
4166
+ * priority chain). Provide a redacted/short version to avoid sending
4167
+ * the full directive in subsequent turns.
4168
+ */
4169
+ llmContent?: string;
4170
+ /**
4171
+ * Optional message ID. If omitted, an assistant id is auto-generated.
4172
+ */
4173
+ id?: string;
4174
+ /**
4175
+ * Optional creation timestamp (ISO string). If omitted, uses current time.
4176
+ */
4177
+ createdAt?: string;
4178
+ /**
4179
+ * Optional sequence number for ordering.
4180
+ */
4181
+ sequence?: number;
4182
+ };
4116
4183
  type PersonaArtifactRecord = {
4117
4184
  id: string;
4118
4185
  artifactType: PersonaArtifactKind;
@@ -4480,6 +4547,12 @@ type Controller = {
4480
4547
  * Inject multiple messages in a single batch with one sort and one render pass.
4481
4548
  */
4482
4549
  injectMessageBatch: (optionsList: InjectMessageOptions[]) => AgentWidgetMessage[];
4550
+ /**
4551
+ * Convenience method for injecting an assistant message that renders as a
4552
+ * registered component — same shape Persona produces from a streamed
4553
+ * `{ "text": "...", "component": "...", "props": {...} }` payload.
4554
+ */
4555
+ injectComponentDirective: (options: InjectComponentDirectiveOptions) => AgentWidgetMessage;
4483
4556
  /**
4484
4557
  * @deprecated Use injectMessage() instead.
4485
4558
  */
@@ -4097,6 +4097,22 @@ type InjectMessageOptions = {
4097
4097
  * Consumers can detect this in `messageTransform` to render custom UI.
4098
4098
  */
4099
4099
  voiceProcessing?: boolean;
4100
+ /**
4101
+ * Raw structured payload (typically a JSON string) representing the
4102
+ * full directive that produced this message — e.g. `{ "text": "...",
4103
+ * "component": "Foo", "props": {...} }`.
4104
+ *
4105
+ * Mirrors the field populated by stream parsers during normal LLM
4106
+ * responses. Set this when injecting a message that should render as a
4107
+ * component directive (`hasComponentDirective` /
4108
+ * `extractComponentDirectiveFromMessage` look at `rawContent` first).
4109
+ *
4110
+ * Priority for the API payload remains:
4111
+ * `contentParts > llmContent > rawContent > content`. Pass `llmContent`
4112
+ * alongside `rawContent` if the LLM should see something other than the
4113
+ * raw directive.
4114
+ */
4115
+ rawContent?: string;
4100
4116
  };
4101
4117
  /**
4102
4118
  * Options for injecting assistant messages (most common case).
@@ -4113,6 +4129,57 @@ type InjectUserMessageOptions = Omit<InjectMessageOptions, "role">;
4113
4129
  * Role defaults to 'system'.
4114
4130
  */
4115
4131
  type InjectSystemMessageOptions = Omit<InjectMessageOptions, "role">;
4132
+ /**
4133
+ * Options for injecting an assistant message that renders as a component
4134
+ * directive — sugar over `injectAssistantMessage` for the common case of
4135
+ * "render this registered component, same as if the LLM had emitted it".
4136
+ *
4137
+ * Equivalent to calling `injectAssistantMessage({ content: text, rawContent:
4138
+ * JSON.stringify({ text, component, props }), llmContent })`.
4139
+ *
4140
+ * @example
4141
+ * widget.injectComponentDirective({
4142
+ * component: "DynamicForm",
4143
+ * props: { title: "Book a demo", fields: [...] },
4144
+ * text: "Share your details to book a demo.",
4145
+ * llmContent: "[Showed booking form]"
4146
+ * });
4147
+ */
4148
+ type InjectComponentDirectiveOptions = {
4149
+ /**
4150
+ * Name of a renderer registered via `componentRegistry.register(...)`.
4151
+ */
4152
+ component: string;
4153
+ /**
4154
+ * Props passed to the component renderer.
4155
+ */
4156
+ props?: Record<string, unknown>;
4157
+ /**
4158
+ * Bubble copy displayed above (or with) the rendered component.
4159
+ * Mirrors the `text` field in a streamed JSON directive.
4160
+ * @default ""
4161
+ */
4162
+ text?: string;
4163
+ /**
4164
+ * Content sent to the LLM in API requests. When omitted, the raw
4165
+ * directive JSON is what the LLM would see (per the standard
4166
+ * priority chain). Provide a redacted/short version to avoid sending
4167
+ * the full directive in subsequent turns.
4168
+ */
4169
+ llmContent?: string;
4170
+ /**
4171
+ * Optional message ID. If omitted, an assistant id is auto-generated.
4172
+ */
4173
+ id?: string;
4174
+ /**
4175
+ * Optional creation timestamp (ISO string). If omitted, uses current time.
4176
+ */
4177
+ createdAt?: string;
4178
+ /**
4179
+ * Optional sequence number for ordering.
4180
+ */
4181
+ sequence?: number;
4182
+ };
4116
4183
  type PersonaArtifactRecord = {
4117
4184
  id: string;
4118
4185
  artifactType: PersonaArtifactKind;
@@ -4480,6 +4547,12 @@ type Controller = {
4480
4547
  * Inject multiple messages in a single batch with one sort and one render pass.
4481
4548
  */
4482
4549
  injectMessageBatch: (optionsList: InjectMessageOptions[]) => AgentWidgetMessage[];
4550
+ /**
4551
+ * Convenience method for injecting an assistant message that renders as a
4552
+ * registered component — same shape Persona produces from a streamed
4553
+ * `{ "text": "...", "component": "...", "props": {...} }` payload.
4554
+ */
4555
+ injectComponentDirective: (options: InjectComponentDirectiveOptions) => AgentWidgetMessage;
4483
4556
  /**
4484
4557
  * @deprecated Use injectMessage() instead.
4485
4558
  */
@@ -7394,7 +7394,8 @@ var AgentWidgetSession = class _AgentWidgetSession {
7394
7394
  createdAt,
7395
7395
  sequence,
7396
7396
  streaming = false,
7397
- voiceProcessing
7397
+ voiceProcessing,
7398
+ rawContent
7398
7399
  } = options;
7399
7400
  const messageId = id != null ? id : role === "user" ? generateUserMessageId() : role === "assistant" ? generateAssistantMessageId() : `system-${Date.now()}-${Math.random().toString(16).slice(2)}`;
7400
7401
  const message = {
@@ -7407,7 +7408,8 @@ var AgentWidgetSession = class _AgentWidgetSession {
7407
7408
  // Only include optional fields if provided
7408
7409
  ...llmContent !== void 0 && { llmContent },
7409
7410
  ...contentParts !== void 0 && { contentParts },
7410
- ...voiceProcessing !== void 0 && { voiceProcessing }
7411
+ ...voiceProcessing !== void 0 && { voiceProcessing },
7412
+ ...rawContent !== void 0 && { rawContent }
7411
7413
  };
7412
7414
  this.upsertMessage(message);
7413
7415
  return message;
@@ -7472,7 +7474,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7472
7474
  id,
7473
7475
  createdAt,
7474
7476
  sequence,
7475
- streaming = false
7477
+ streaming = false,
7478
+ voiceProcessing,
7479
+ rawContent
7476
7480
  } = options;
7477
7481
  const messageId = id != null ? id : role === "user" ? generateUserMessageId() : role === "assistant" ? generateAssistantMessageId() : `system-${Date.now()}-${Math.random().toString(16).slice(2)}`;
7478
7482
  const message = {
@@ -7483,7 +7487,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7483
7487
  sequence: sequence != null ? sequence : this.nextSequence(),
7484
7488
  streaming,
7485
7489
  ...llmContent !== void 0 && { llmContent },
7486
- ...contentParts !== void 0 && { contentParts }
7490
+ ...contentParts !== void 0 && { contentParts },
7491
+ ...voiceProcessing !== void 0 && { voiceProcessing },
7492
+ ...rawContent !== void 0 && { rawContent }
7487
7493
  };
7488
7494
  results.push(message);
7489
7495
  }
@@ -7491,6 +7497,48 @@ var AgentWidgetSession = class _AgentWidgetSession {
7491
7497
  this.callbacks.onMessagesChanged([...this.messages]);
7492
7498
  return results;
7493
7499
  }
7500
+ /**
7501
+ * Convenience method for injecting a registered component directive as
7502
+ * an assistant message — the same shape Persona produces from a streamed
7503
+ * `{ "text": "...", "component": "...", "props": {...} }` payload.
7504
+ *
7505
+ * Sets `content` to `text`, `rawContent` to the JSON directive (so
7506
+ * `extractComponentDirectiveFromMessage` can find it), and forwards
7507
+ * `llmContent` / `id` / `createdAt` / `sequence`.
7508
+ *
7509
+ * @example
7510
+ * session.injectComponentDirective({
7511
+ * component: "DynamicForm",
7512
+ * props: { title: "Book a demo", fields: [...] },
7513
+ * text: "Share your details to book a demo.",
7514
+ * llmContent: "[Showed booking form]"
7515
+ * });
7516
+ */
7517
+ injectComponentDirective(options) {
7518
+ const {
7519
+ component,
7520
+ props = {},
7521
+ text = "",
7522
+ llmContent,
7523
+ id,
7524
+ createdAt,
7525
+ sequence
7526
+ } = options;
7527
+ const directive = {
7528
+ text,
7529
+ component,
7530
+ props
7531
+ };
7532
+ return this.injectMessage({
7533
+ role: "assistant",
7534
+ content: text,
7535
+ rawContent: JSON.stringify(directive),
7536
+ ...llmContent !== void 0 && { llmContent },
7537
+ ...id !== void 0 && { id },
7538
+ ...createdAt !== void 0 && { createdAt },
7539
+ ...sequence !== void 0 && { sequence }
7540
+ });
7541
+ }
7494
7542
  async sendMessage(rawInput, options) {
7495
7543
  var _a, _b, _c, _d, _e;
7496
7544
  const input = rawInput.trim();
@@ -15329,24 +15377,39 @@ function renderComponentDirective(directive, options) {
15329
15377
  return null;
15330
15378
  }
15331
15379
  }
15380
+ function selectDirectiveSource(message) {
15381
+ if (typeof message.rawContent === "string" && message.rawContent.length > 0) {
15382
+ return message.rawContent;
15383
+ }
15384
+ if (typeof message.content === "string") {
15385
+ const trimmed = message.content.trim();
15386
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
15387
+ return message.content;
15388
+ }
15389
+ }
15390
+ return null;
15391
+ }
15332
15392
  function hasComponentDirective(message) {
15333
- if (!message.rawContent) return false;
15393
+ const source = selectDirectiveSource(message);
15394
+ if (!source) return false;
15334
15395
  try {
15335
- const parsed = JSON.parse(message.rawContent);
15396
+ const parsed = JSON.parse(source);
15336
15397
  return typeof parsed === "object" && parsed !== null && "component" in parsed && typeof parsed.component === "string";
15337
15398
  } catch {
15338
15399
  return false;
15339
15400
  }
15340
15401
  }
15341
15402
  function extractComponentDirectiveFromMessage(message) {
15342
- if (!message.rawContent) return null;
15403
+ const source = selectDirectiveSource(message);
15404
+ if (!source) return null;
15343
15405
  try {
15344
- const parsed = JSON.parse(message.rawContent);
15406
+ const parsed = JSON.parse(source);
15345
15407
  if (typeof parsed === "object" && parsed !== null && "component" in parsed && typeof parsed.component === "string") {
15408
+ const directive = parsed;
15346
15409
  return {
15347
- component: parsed.component,
15348
- props: parsed.props && typeof parsed.props === "object" && parsed.props !== null ? parsed.props : {},
15349
- raw: message.rawContent
15410
+ component: directive.component,
15411
+ props: directive.props && typeof directive.props === "object" && directive.props !== null ? directive.props : {},
15412
+ raw: source
15350
15413
  };
15351
15414
  }
15352
15415
  } catch {
@@ -20746,6 +20809,12 @@ var createAgentExperience = (mount, initialConfig, runtimeOptions) => {
20746
20809
  }
20747
20810
  return session.injectMessageBatch(optionsList);
20748
20811
  },
20812
+ injectComponentDirective(options) {
20813
+ if (!open && isPanelToggleable()) {
20814
+ setOpenState(true, "system");
20815
+ }
20816
+ return session.injectComponentDirective(options);
20817
+ },
20749
20818
  /** @deprecated Use injectMessage() instead */
20750
20819
  injectTestMessage(event) {
20751
20820
  if (!open && isPanelToggleable()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.19.0",
3
+ "version": "3.20.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",
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export type {
61
61
  InjectAssistantMessageOptions,
62
62
  InjectUserMessageOptions,
63
63
  InjectSystemMessageOptions,
64
+ InjectComponentDirectiveOptions,
64
65
  // Loading indicator types
65
66
  LoadingIndicatorRenderContext,
66
67
  AgentWidgetLoadingIndicatorConfig,
@@ -244,6 +244,129 @@ describe('AgentWidgetSession - Message Injection', () => {
244
244
  expect(messages[0].content).toBe('Legacy message');
245
245
  });
246
246
  });
247
+
248
+ describe('rawContent forwarding', () => {
249
+ it('preserves rawContent on injected messages', () => {
250
+ const directive = JSON.stringify({
251
+ text: 'Booking form',
252
+ component: 'BookingForm',
253
+ props: { title: 'Schedule' }
254
+ });
255
+
256
+ const result = session.injectMessage({
257
+ role: 'assistant',
258
+ content: 'Booking form',
259
+ rawContent: directive
260
+ });
261
+
262
+ expect(result.rawContent).toBe(directive);
263
+ expect(messages[0].rawContent).toBe(directive);
264
+ });
265
+
266
+ it('forwards rawContent through injectAssistantMessage', () => {
267
+ const directive = JSON.stringify({ text: 'Form', component: 'Form', props: {} });
268
+
269
+ const result = session.injectAssistantMessage({
270
+ content: 'Form',
271
+ rawContent: directive
272
+ });
273
+
274
+ expect(result.rawContent).toBe(directive);
275
+ });
276
+
277
+ it('forwards rawContent through injectMessageBatch', () => {
278
+ const directiveA = JSON.stringify({ text: 'A', component: 'CompA', props: {} });
279
+ const directiveB = JSON.stringify({ text: 'B', component: 'CompB', props: {} });
280
+
281
+ const results = session.injectMessageBatch([
282
+ { role: 'assistant', content: 'A', rawContent: directiveA },
283
+ { role: 'assistant', content: 'B', rawContent: directiveB }
284
+ ]);
285
+
286
+ expect(results[0].rawContent).toBe(directiveA);
287
+ expect(results[1].rawContent).toBe(directiveB);
288
+ });
289
+
290
+ it('omits rawContent when not provided', () => {
291
+ const result = session.injectMessage({
292
+ role: 'assistant',
293
+ content: 'plain'
294
+ });
295
+
296
+ expect(result.rawContent).toBeUndefined();
297
+ });
298
+ });
299
+
300
+ describe('injectComponentDirective', () => {
301
+ it('builds rawContent from component + props + text', () => {
302
+ const result = session.injectComponentDirective({
303
+ component: 'DynamicForm',
304
+ props: { title: 'Book a demo', fields: [{ label: 'Email' }] },
305
+ text: 'Share your details to book a demo.'
306
+ });
307
+
308
+ expect(result.role).toBe('assistant');
309
+ expect(result.content).toBe('Share your details to book a demo.');
310
+ expect(result.id).toMatch(/^ast_/);
311
+ expect(result.rawContent).toBeDefined();
312
+
313
+ const parsed = JSON.parse(result.rawContent as string);
314
+ expect(parsed).toEqual({
315
+ text: 'Share your details to book a demo.',
316
+ component: 'DynamicForm',
317
+ props: { title: 'Book a demo', fields: [{ label: 'Email' }] }
318
+ });
319
+ });
320
+
321
+ it('defaults text to empty string and props to {}', () => {
322
+ const result = session.injectComponentDirective({ component: 'DynamicForm' });
323
+
324
+ expect(result.content).toBe('');
325
+ const parsed = JSON.parse(result.rawContent as string);
326
+ expect(parsed).toEqual({ text: '', component: 'DynamicForm', props: {} });
327
+ });
328
+
329
+ it('forwards llmContent for redacted LLM context', () => {
330
+ const result = session.injectComponentDirective({
331
+ component: 'DynamicForm',
332
+ props: { title: 'Book a demo' },
333
+ text: 'Booking form below.',
334
+ llmContent: '[Showed booking form]'
335
+ });
336
+
337
+ expect(result.llmContent).toBe('[Showed booking form]');
338
+ });
339
+
340
+ it('honors custom id, createdAt, sequence', () => {
341
+ const result = session.injectComponentDirective({
342
+ component: 'DynamicForm',
343
+ id: 'my-form-1',
344
+ createdAt: '2026-01-01T00:00:00.000Z',
345
+ sequence: 999
346
+ });
347
+
348
+ expect(result.id).toBe('my-form-1');
349
+ expect(result.createdAt).toBe('2026-01-01T00:00:00.000Z');
350
+ expect(result.sequence).toBe(999);
351
+ });
352
+
353
+ it('upserts an existing directive by id', () => {
354
+ session.injectComponentDirective({
355
+ component: 'DynamicForm',
356
+ props: { title: 'v1' },
357
+ id: 'reuse-me'
358
+ });
359
+ session.injectComponentDirective({
360
+ component: 'DynamicForm',
361
+ props: { title: 'v2' },
362
+ id: 'reuse-me'
363
+ });
364
+
365
+ expect(messages).toHaveLength(1);
366
+ const parsed = JSON.parse(messages[0].rawContent as string);
367
+ expect(parsed.props.title).toBe('v2');
368
+ });
369
+ });
247
370
  });
248
371
 
249
372
  describe('AgentWidgetSession - cancel()', () => {