@runtypelabs/persona 3.28.0 → 3.29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.28.0",
3
+ "version": "3.29.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",
@@ -36,4 +36,47 @@ describe("codegen subpath", () => {
36
36
  expect(code).toContain(`@runtypelabs/persona@${VERSION}/dist/install.global.js`);
37
37
  expect(code).not.toContain("@latest");
38
38
  });
39
+
40
+ describe("mount target option", () => {
41
+ it("defaults to body when no target is given", () => {
42
+ expect(fromSubpath(config, "react-component")).toContain("target: 'body'");
43
+ expect(fromSubpath(config, "script-manual")).toContain("target: 'body'");
44
+ // installer omits the target key entirely (installer falls back to body)
45
+ expect(fromSubpath(config, "script-installer")).not.toContain('"target"');
46
+ });
47
+
48
+ it("emits the selector as the initAgentWidget target for code formats", () => {
49
+ const opts = { target: "#chat" };
50
+ expect(fromSubpath(config, "esm", opts)).toContain("target: '#chat'");
51
+ expect(fromSubpath(config, "react-component", opts)).toContain("target: '#chat'");
52
+ expect(fromSubpath(config, "script-manual", opts)).toContain("target: '#chat'");
53
+ });
54
+
55
+ it("serializes the selector as a top-level installer option (sibling of config)", () => {
56
+ // install.ts reads the mount point from the TOP LEVEL of data-config, not
57
+ // from inside the widget config. Parse the emitted data-config and assert
58
+ // the structure so a regression to the nested form is caught.
59
+ const code = fromSubpath(config, "script-installer", { target: "#chat" });
60
+ const json = code.match(/data-config='([^']*)'/)?.[1];
61
+ expect(json).toBeTruthy();
62
+ const parsed = JSON.parse(json!);
63
+ expect(parsed.target).toBe("#chat");
64
+ expect(parsed.config).toBeTruthy();
65
+ // target must NOT be buried inside the widget config (where it's ignored)
66
+ expect(parsed.config.target).toBeUndefined();
67
+ });
68
+
69
+ it("keeps target as a top-level sibling alongside windowKey", () => {
70
+ const code = fromSubpath(config, "script-installer", { target: "#chat", windowKey: "myWidget" });
71
+ const parsed = JSON.parse(code.match(/data-config='([^']*)'/)![1]);
72
+ expect(parsed.target).toBe("#chat");
73
+ expect(parsed.windowKey).toBe("myWidget");
74
+ expect(parsed.config).toBeTruthy();
75
+ });
76
+
77
+ it("escapes single quotes in the selector so it can't break the literal", () => {
78
+ const code = fromSubpath(config, "react-component", { target: "[data-x='y']" });
79
+ expect(code).toContain("target: '[data-x=\\'y\\']'");
80
+ });
81
+ });
39
82
  });
@@ -141,6 +141,16 @@ export type CodeGeneratorOptions = {
141
141
  * Only affects script formats (script-installer, script-manual, script-advanced).
142
142
  */
143
143
  windowKey?: string;
144
+
145
+ /**
146
+ * CSS selector for the mount target. When omitted, the widget mounts on
147
+ * `body` (the existing default). When provided, the generated snippet mounts
148
+ * into that element: ESM / React / manual / advanced formats emit it as the
149
+ * `target` argument to `initAgentWidget()`, and `script-installer` serializes
150
+ * it into `data-config` (the installer reads `config.target`).
151
+ * @default "body"
152
+ */
153
+ target?: string;
144
154
  };
145
155
 
146
156
  // Internal type for normalized hooks (always strings)
@@ -540,6 +550,15 @@ function appendSerializableObjectBlock(
540
550
  lines.push(`${indent}},`);
541
551
  }
542
552
 
553
+ /**
554
+ * Resolve the mount-target selector for a single-quoted JS string context.
555
+ * Defaults to `body` and escapes backslashes / single quotes so an arbitrary
556
+ * selector can't break out of the emitted `target: '...'` literal.
557
+ */
558
+ function emitTargetSelector(options?: CodeGeneratorOptions): string {
559
+ return (options?.target ?? "body").replace(/\\/g, "\\\\").replace(/'/g, "\\'");
560
+ }
561
+
543
562
  export function generateCodeSnippet(
544
563
  config: any,
545
564
  format: CodeFormat = "esm",
@@ -580,7 +599,7 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
580
599
  "import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';",
581
600
  "",
582
601
  "initAgentWidget({",
583
- " target: 'body',",
602
+ ` target: '${emitTargetSelector(options)}',`,
584
603
  " config: {"
585
604
  ];
586
605
 
@@ -726,7 +745,7 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
726
745
  " let handle: AgentWidgetInitHandle | null = null;",
727
746
  "",
728
747
  " handle = initAgentWidget({",
729
- " target: 'body',",
748
+ ` target: '${emitTargetSelector(options)}',`,
730
749
  " config: {"
731
750
  ];
732
751
 
@@ -990,7 +1009,7 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
990
1009
  " };",
991
1010
  "",
992
1011
  " handle = initAgentWidget({",
993
- " target: 'body',",
1012
+ ` target: '${emitTargetSelector(options)}',`,
994
1013
  " config: {"
995
1014
  ];
996
1015
 
@@ -1356,10 +1375,20 @@ function buildSerializableConfig(config: any): Record<string, any> {
1356
1375
  function generateScriptInstallerCode(config: any, options?: CodeGeneratorOptions): string {
1357
1376
  const serializableConfig = buildSerializableConfig(config);
1358
1377
 
1359
- // When windowKey is provided, nest the widget config under `config` so the
1360
- // install script's parsedConfig.config detection picks it up alongside windowKey.
1361
- const payload = options?.windowKey
1362
- ? { config: serializableConfig, windowKey: options.windowKey }
1378
+ // `windowKey` and `target` are INSTALLER options, not widget-config fields.
1379
+ // install.ts reads them from the top level of data-config: the nested-`config`
1380
+ // branch hoists every sibling key (Object.assign(scriptConfig, parsed)), while
1381
+ // a flat payload becomes the widget config wholesale and its siblings are
1382
+ // ignored. So whenever either is set we emit the nested `{ config, ...}` form
1383
+ // with the option as a SIBLING of `config` — nesting `target` inside the
1384
+ // widget config would leave the widget mounted on `body`.
1385
+ const needsWrapper = Boolean(options?.windowKey || options?.target);
1386
+ const payload = needsWrapper
1387
+ ? {
1388
+ config: serializableConfig,
1389
+ ...(options?.windowKey ? { windowKey: options.windowKey } : {}),
1390
+ ...(options?.target ? { target: options.target } : {}),
1391
+ }
1363
1392
  : serializableConfig;
1364
1393
 
1365
1394
  // Escape single quotes in JSON for HTML attribute
@@ -1383,7 +1412,7 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1383
1412
  "<!-- Initialize widget -->",
1384
1413
  "<script>",
1385
1414
  " var handle = window.AgentWidget.initAgentWidget({",
1386
- " target: 'body',",
1415
+ ` target: '${emitTargetSelector(options)}',`,
1387
1416
  ...(options?.windowKey ? [` windowKey: '${options.windowKey}',`] : []),
1388
1417
  " config: {"
1389
1418
  ];
@@ -1735,7 +1764,7 @@ function generateScriptAdvancedCode(config: any, options?: CodeGeneratorOptions)
1735
1764
  "",
1736
1765
  " // Initialize widget",
1737
1766
  " var handle = agentWidget.initAgentWidget({",
1738
- " target: 'body',",
1767
+ ` target: '${emitTargetSelector(options)}',`,
1739
1768
  " useShadowDom: false,",
1740
1769
  ...(options?.windowKey ? [` windowKey: '${options.windowKey}',`] : []),
1741
1770
  " config: widgetConfig",