@runtypelabs/persona 4.17.1 → 4.18.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 (158) hide show
  1. package/dist/chunk-3I2S3I5P.js +1 -0
  2. package/dist/chunk-76IELU7F.js +1 -0
  3. package/dist/chunk-EFJBUH5F.js +1 -0
  4. package/dist/chunk-PDPVORUF.js +1 -0
  5. package/dist/chunk-Q735PSZL.js +1 -0
  6. package/dist/chunk-VK6JOIYJ.js +1 -0
  7. package/dist/codegen.cjs +4 -4
  8. package/dist/codegen.js +4 -4
  9. package/dist/{context-mentions-BTSTIBFC.js → context-mentions-MJL6S6P2.js} +1 -1
  10. package/dist/context-mentions.cjs +9 -8
  11. package/dist/context-mentions.d.cts +1212 -14
  12. package/dist/context-mentions.d.ts +1212 -14
  13. package/dist/context-mentions.js +9 -8
  14. package/dist/event-stream-view-4EZ7BGFK.js +1 -0
  15. package/dist/event-stream-view.cjs +129 -0
  16. package/dist/event-stream-view.d.cts +8384 -0
  17. package/dist/event-stream-view.d.ts +8384 -0
  18. package/dist/event-stream-view.js +129 -0
  19. package/dist/history-view-BD6OXLNA.js +898 -0
  20. package/dist/history-view.cjs +898 -0
  21. package/dist/history-view.d.cts +1461 -0
  22. package/dist/history-view.d.ts +1461 -0
  23. package/dist/history-view.js +898 -0
  24. package/dist/index.cjs +77 -77
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1987 -17
  27. package/dist/index.d.ts +1987 -17
  28. package/dist/index.global.js +67 -66
  29. package/dist/index.global.js.map +1 -1
  30. package/dist/index.js +77 -77
  31. package/dist/index.js.map +1 -1
  32. package/dist/launcher.global.js +5 -4
  33. package/dist/launcher.global.js.map +1 -1
  34. package/dist/plugin-kit.cjs +1 -1
  35. package/dist/plugin-kit.d.cts +0 -8
  36. package/dist/plugin-kit.d.ts +0 -8
  37. package/dist/plugin-kit.js +1 -1
  38. package/dist/smart-dom-reader.d.cts +1212 -14
  39. package/dist/smart-dom-reader.d.ts +1212 -14
  40. package/dist/theme-editor-preview.cjs +970 -73
  41. package/dist/theme-editor-preview.d.cts +1331 -15
  42. package/dist/theme-editor-preview.d.ts +1331 -15
  43. package/dist/theme-editor-preview.js +81 -81
  44. package/dist/theme-editor.cjs +7 -7
  45. package/dist/theme-editor.d.cts +1215 -15
  46. package/dist/theme-editor.d.ts +1215 -15
  47. package/dist/theme-editor.js +9 -9
  48. package/dist/theme-reference.cjs +1 -1
  49. package/dist/theme-reference.d.cts +226 -7
  50. package/dist/theme-reference.d.ts +226 -7
  51. package/dist/theme-reference.js +1 -1
  52. package/dist/widget.css +1 -1
  53. package/package.json +16 -4
  54. package/src/client-history-rest.test.ts +1034 -0
  55. package/src/client-visitor-history.test.ts +596 -0
  56. package/src/client.ts +1040 -24
  57. package/src/components/artifact-inline.ts +8 -1
  58. package/src/components/artifact-pane.test.ts +278 -1
  59. package/src/components/artifact-pane.ts +135 -67
  60. package/src/components/artifact-preview.test.ts +14 -1
  61. package/src/components/artifact-preview.ts +9 -0
  62. package/src/components/header-builder.ts +3 -0
  63. package/src/components/header-layouts.test.ts +171 -7
  64. package/src/components/header-layouts.ts +83 -53
  65. package/src/components/header-parts.test.ts +154 -1
  66. package/src/components/header-parts.ts +266 -138
  67. package/src/components/history-confirm-dialog.ts +177 -0
  68. package/src/components/history-shell-copy.ts +99 -0
  69. package/src/components/history-view/announcer.ts +40 -0
  70. package/src/components/history-view/copy.ts +105 -0
  71. package/src/components/history-view/css.ts +1078 -0
  72. package/src/components/history-view/grouping.test.ts +142 -0
  73. package/src/components/history-view/grouping.ts +142 -0
  74. package/src/components/history-view/icons.ts +73 -0
  75. package/src/components/history-view/rows.ts +341 -0
  76. package/src/components/history-view/state.ts +309 -0
  77. package/src/components/history-view.test.ts +2660 -0
  78. package/src/components/history-view.ts +1891 -0
  79. package/src/components/launcher-surface.test.ts +133 -0
  80. package/src/components/launcher.test.ts +18 -0
  81. package/src/components/launcher.ts +7 -7
  82. package/src/components/panel.ts +17 -7
  83. package/src/components/welcome.ts +72 -2
  84. package/src/defaults.ts +36 -3
  85. package/src/event-stream-view-bundle.test.ts +69 -0
  86. package/src/event-stream-view-entry.ts +19 -0
  87. package/src/event-stream-view-loader.ts +31 -0
  88. package/src/event-stream-view.ts +19 -0
  89. package/src/generated/runtype-openapi-contract.ts +1 -0
  90. package/src/history-render.ts +325 -0
  91. package/src/history-view-bundle.test.ts +78 -0
  92. package/src/history-view-entry.ts +41 -0
  93. package/src/history-view-loader.test.ts +30 -0
  94. package/src/history-view-loader.ts +32 -0
  95. package/src/history-view.ts +28 -0
  96. package/src/index-core.ts +50 -2
  97. package/src/index-global.ts +55 -0
  98. package/src/internal/demo-history-provider.test.ts +402 -0
  99. package/src/internal/demo-history-provider.ts +689 -0
  100. package/src/internal/history-provider-registry.ts +25 -0
  101. package/src/internal/history-provider.ts +150 -0
  102. package/src/internal/runtype-history-provider.test.ts +313 -0
  103. package/src/internal/runtype-history-provider.ts +240 -0
  104. package/src/plugin-kit.test.ts +16 -0
  105. package/src/plugin-kit.ts +13 -5
  106. package/src/plugins/types.ts +177 -1
  107. package/src/session-history.test.ts +1397 -0
  108. package/src/session.ts +1200 -26
  109. package/src/styles/widget-styles.test.ts +133 -0
  110. package/src/styles/widget.css +227 -8
  111. package/src/theme-editor/preview-utils.test.ts +19 -0
  112. package/src/theme-editor/preview-utils.ts +15 -1
  113. package/src/theme-editor/preview.ts +7 -1
  114. package/src/theme-editor/role-mappings.ts +4 -1
  115. package/src/theme-editor/sections.test.ts +168 -1
  116. package/src/theme-editor/sections.ts +136 -7
  117. package/src/theme-editor/webmcp/summary.ts +16 -2
  118. package/src/theme-reference.ts +20 -6
  119. package/src/types/theme.ts +213 -7
  120. package/src/types.ts +927 -7
  121. package/src/ui.artifact-rail-resize.test.ts +235 -0
  122. package/src/ui.attachments-drop.test.ts +1 -0
  123. package/src/ui.header-update-stability.test.ts +2 -2
  124. package/src/ui.history-chunk.test.ts +72 -0
  125. package/src/ui.history-config-provider.test.ts +318 -0
  126. package/src/ui.history-controller.test.ts +711 -0
  127. package/src/ui.history-lifecycle.test.ts +456 -0
  128. package/src/ui.history-render-hooks.test.ts +904 -0
  129. package/src/ui.history-shell.test.ts +1887 -0
  130. package/src/ui.rail-overlay.test.ts +573 -0
  131. package/src/ui.rail-resize.test.ts +284 -0
  132. package/src/ui.rebuild-composer.test.ts +21 -0
  133. package/src/ui.render-welcome.test.ts +42 -0
  134. package/src/ui.shortcuts.test.ts +166 -0
  135. package/src/ui.ts +3071 -240
  136. package/src/utils/__fixtures__/client-conversation-history-wire.json +108 -0
  137. package/src/utils/artifact-file.ts +15 -0
  138. package/src/utils/artifact-gate.test.ts +4 -0
  139. package/src/utils/artifact-gate.ts +13 -17
  140. package/src/utils/artifact-resize.test.ts +28 -0
  141. package/src/utils/artifact-resize.ts +9 -2
  142. package/src/utils/buttons.ts +188 -1
  143. package/src/utils/config-merge.test.ts +91 -0
  144. package/src/utils/dropdown.ts +10 -1
  145. package/src/utils/history-messages.test.ts +284 -0
  146. package/src/utils/history-messages.ts +284 -0
  147. package/src/utils/icons.ts +3 -0
  148. package/src/utils/shortcuts.test.ts +288 -0
  149. package/src/utils/shortcuts.ts +221 -0
  150. package/src/utils/theme.test.ts +501 -4
  151. package/src/utils/theme.ts +42 -3
  152. package/src/utils/tokens.ts +153 -15
  153. package/src/utils/tooltip-timing.ts +4 -0
  154. package/src/utils/tooltip.test.ts +249 -7
  155. package/src/utils/tooltip.ts +146 -4
  156. package/src/utils/visitor-store.test.ts +425 -0
  157. package/src/utils/visitor-store.ts +337 -0
  158. package/dist/chunk-QJ4LV2KW.js +0 -1
@@ -1,7 +1,12 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+
3
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
7
  import { createLauncherSurface } from "./launcher";
8
+ import { mount as mountCriticalLauncher } from "../launcher-global";
9
+ import { createAgentExperience } from "../ui";
5
10
  import type {
6
11
  AgentWidgetConfig,
7
12
  AgentWidgetLauncherTeaserConfig,
@@ -401,3 +406,131 @@ describe("createLauncherSurface", () => {
401
406
  expect(document.body.children.length).toBe(0);
402
407
  });
403
408
  });
409
+
410
+ /**
411
+ * The deferred install paints the launcher from the critical bundle and swaps
412
+ * to the full widget on first click, so any glyph styling must reach both
413
+ * surfaces from the one stylesheet or the swap flickers.
414
+ */
415
+ describe("launcher icon stroke parity", () => {
416
+ // jsdom leaves `import.meta.url` an http URL, so resolve from the package root.
417
+ const widgetCss = readFileSync(
418
+ resolve(process.cwd(), "src/styles/widget.css"),
419
+ "utf8"
420
+ );
421
+ // The SHIPPED selector, read back out of widget.css by its declaration, so
422
+ // a rescope of the real rule fails here instead of slipping past a
423
+ // hardcoded copy of the selector.
424
+ const strokeDeclarationAt = widgetCss.indexOf(
425
+ "stroke-width: var(--persona-components-launcher-iconStrokeWidth, 1.5)"
426
+ );
427
+ const strokeSelectorStart = widgetCss.lastIndexOf("}", strokeDeclarationAt) + 1;
428
+ const STROKE_RULE = widgetCss
429
+ .slice(strokeSelectorStart, widgetCss.indexOf("{", strokeSelectorStart))
430
+ .replace(/\/\*[\s\S]*?\*\//g, "")
431
+ .trim();
432
+
433
+ beforeEach(() => {
434
+ window.scrollTo = vi.fn();
435
+ window.localStorage.clear();
436
+ });
437
+
438
+ afterEach(() => {
439
+ document.body.innerHTML = "";
440
+ vi.restoreAllMocks();
441
+ });
442
+
443
+ it("renders one glyph the shared stroke rule reaches from both surfaces", () => {
444
+ const config = {
445
+ apiUrl: "https://api.example.com/chat",
446
+ persistState: { keyPrefix: nextPrefix() },
447
+ launcher: {
448
+ agentIconName: "message-circle",
449
+ callToActionIconName: "arrow-up-right",
450
+ },
451
+ };
452
+
453
+ const host = document.createElement("div");
454
+ document.body.appendChild(host);
455
+ const controller = createAgentExperience(host, config);
456
+
457
+ const criticalTarget = document.createElement("div");
458
+ document.body.appendChild(criticalTarget);
459
+ const critical = mountCriticalLauncher({
460
+ target: criticalTarget,
461
+ config,
462
+ onOpen: () => {},
463
+ });
464
+
465
+ const iconSvg = "[data-role='launcher-icon'] > svg";
466
+ const eager = host.querySelector<SVGElement>(iconSvg);
467
+ const deferred = criticalTarget.querySelector<SVGElement>(iconSvg);
468
+
469
+ expect(eager).not.toBeNull();
470
+ expect(deferred).not.toBeNull();
471
+ expect(deferred!.outerHTML).toBe(eager!.outerHTML);
472
+ // matches() resolves the descendant combinator against each surface's own
473
+ // ancestors, and STROKE_RULE is the selector parsed from widget.css, so
474
+ // this proves the shipped rule selects the glyph in both surfaces.
475
+ expect(eager!.matches(STROKE_RULE)).toBe(true);
476
+ expect(deferred!.matches(STROKE_RULE)).toBe(true);
477
+
478
+ // One pill, one line weight: the call-to-action arrow follows the same
479
+ // rule as the agent glyph on both surfaces.
480
+ const ctaSvg = "[data-role='launcher-call-to-action-icon'] > svg";
481
+ const eagerCta = host.querySelector<SVGElement>(ctaSvg);
482
+ const deferredCta = criticalTarget.querySelector<SVGElement>(ctaSvg);
483
+ expect(eagerCta).not.toBeNull();
484
+ expect(deferredCta).not.toBeNull();
485
+ expect(eagerCta!.matches(STROKE_RULE)).toBe(true);
486
+ expect(deferredCta!.matches(STROKE_RULE)).toBe(true);
487
+
488
+ critical.destroy();
489
+ controller.destroy();
490
+ });
491
+
492
+ it("emits the stroke token onto both surfaces' roots", () => {
493
+ const config = {
494
+ apiUrl: "https://api.example.com/chat",
495
+ persistState: { keyPrefix: nextPrefix() },
496
+ launcher: { agentIconName: "message-circle" },
497
+ theme: { components: { launcher: { iconStrokeWidth: "1.25" } } },
498
+ };
499
+
500
+ const host = document.createElement("div");
501
+ document.body.appendChild(host);
502
+ const controller = createAgentExperience(host, config);
503
+
504
+ const criticalTarget = document.createElement("div");
505
+ document.body.appendChild(criticalTarget);
506
+ const critical = mountCriticalLauncher({
507
+ target: criticalTarget,
508
+ config,
509
+ onOpen: () => {},
510
+ });
511
+
512
+ // The theme plumbing is duplicated across the two bundles; this is the
513
+ // claim that breaks if they diverge.
514
+ const fullRoot = host.hasAttribute("data-persona-root")
515
+ ? host
516
+ : host.querySelector<HTMLElement>("[data-persona-root]");
517
+ const criticalRoot = criticalTarget.querySelector<HTMLElement>(
518
+ "[data-persona-root]"
519
+ );
520
+ expect(fullRoot).not.toBeNull();
521
+ expect(criticalRoot).not.toBeNull();
522
+ expect(
523
+ fullRoot!.style.getPropertyValue(
524
+ "--persona-components-launcher-iconStrokeWidth"
525
+ )
526
+ ).toBe("1.25");
527
+ expect(
528
+ criticalRoot!.style.getPropertyValue(
529
+ "--persona-components-launcher-iconStrokeWidth"
530
+ )
531
+ ).toBe("1.25");
532
+
533
+ critical.destroy();
534
+ controller.destroy();
535
+ });
536
+ });
@@ -16,6 +16,24 @@ describe("createLauncherButton", () => {
16
16
  element.remove();
17
17
  });
18
18
 
19
+ it("renders the pill from launcher-specific theme vars", () => {
20
+ const { element, update } = createLauncherButton(undefined, () => {});
21
+ update({});
22
+ expect(element.classList.contains("persona-bg-persona-launcher")).toBe(true);
23
+ expect(
24
+ element
25
+ .querySelector("[data-role='launcher-title']")
26
+ ?.classList.contains("persona-text-persona-launcher")
27
+ ).toBe(true);
28
+ expect(
29
+ element
30
+ .querySelector("[data-role='launcher-subtitle']")
31
+ ?.classList.contains("persona-text-persona-launcher-muted")
32
+ ).toBe(true);
33
+ expect(element.style.border).toContain("--persona-launcher-border");
34
+ element.remove();
35
+ });
36
+
19
37
  it("applies collapsedMaxWidth when set", () => {
20
38
  const { element, update } = createLauncherButton(undefined, () => {});
21
39
  update({
@@ -21,8 +21,8 @@ export const createLauncherButton = (
21
21
  <span class="persona-inline-flex persona-items-center persona-justify-center persona-rounded-full persona-bg-persona-primary persona-text-white" data-role="launcher-icon">💬</span>
22
22
  <img data-role="launcher-image" class="persona-rounded-full persona-object-cover" alt="" style="display:none" />
23
23
  <span class="persona-flex persona-min-w-0 persona-flex-1 persona-flex-col persona-items-start persona-text-left">
24
- <span class="persona-block persona-w-full persona-truncate persona-text-sm persona-font-semibold persona-text-persona-primary" data-role="launcher-title"></span>
25
- <span class="persona-block persona-w-full persona-truncate persona-text-xs persona-text-persona-muted" data-role="launcher-subtitle"></span>
24
+ <span class="persona-block persona-w-full persona-truncate persona-text-sm persona-font-semibold persona-text-persona-launcher" data-role="launcher-title"></span>
25
+ <span class="persona-block persona-w-full persona-truncate persona-text-xs persona-text-persona-launcher-muted" data-role="launcher-subtitle"></span>
26
26
  </span>
27
27
  <span class="persona-ml-2 persona-grid persona-place-items-center persona-rounded-full persona-bg-persona-primary persona-text-persona-call-to-action" data-role="launcher-call-to-action-icon">↗</span>
28
28
  `;
@@ -82,7 +82,7 @@ export const createLauncherButton = (
82
82
  if (launcher.agentIconName) {
83
83
  // Use Lucide icon
84
84
  const iconSizeNum = parseFloat(iconSize) || 24;
85
- const iconSvg = renderLucideIcon(launcher.agentIconName, iconSizeNum * 0.6, "var(--persona-text-inverse, #ffffff)", 2);
85
+ const iconSvg = renderLucideIcon(launcher.agentIconName, iconSizeNum * 0.6, "var(--persona-text-inverse, #ffffff)", 1.5);
86
86
  if (iconSvg) {
87
87
  icon.appendChild(iconSvg);
88
88
  icon.style.display = "";
@@ -166,7 +166,7 @@ export const createLauncherButton = (
166
166
  // Calculate actual icon size by subtracting padding
167
167
  const containerSize = parseFloat(callToActionIconSize) || 24;
168
168
  const iconSize = Math.max(containerSize - paddingTotal, 8); // Ensure minimum size of 8px
169
- const iconSvg = renderLucideIcon(launcher.callToActionIconName, iconSize, "currentColor", 2);
169
+ const iconSvg = renderLucideIcon(launcher.callToActionIconName, iconSize, "currentColor", 1.5);
170
170
  if (iconSvg) {
171
171
  callToActionIconEl.appendChild(iconSvg);
172
172
  } else {
@@ -185,9 +185,9 @@ export const createLauncherButton = (
185
185
  : positionMap["bottom-right"];
186
186
 
187
187
  const floatingBase =
188
- "persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
188
+ "persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-launcher persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
189
189
  const dockedBase =
190
- "persona-relative persona-mt-4 persona-mb-4 persona-mx-auto persona-flex persona-items-center persona-justify-center persona-rounded-launcher persona-bg-persona-surface persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
190
+ "persona-relative persona-mt-4 persona-mb-4 persona-mx-auto persona-flex persona-items-center persona-justify-center persona-rounded-launcher persona-bg-persona-launcher persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
191
191
 
192
192
  button.className = dockedMode ? dockedBase : `${floatingBase} ${positionClass}`;
193
193
 
@@ -196,7 +196,7 @@ export const createLauncherButton = (
196
196
  }
197
197
 
198
198
  // Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
199
- const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
199
+ const defaultBorder = "1px solid var(--persona-launcher-border, var(--persona-border, #e5e7eb))";
200
200
  const defaultShadow = "var(--persona-launcher-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
201
201
 
202
202
  button.style.border = launcher.border ?? defaultBorder;
@@ -21,6 +21,16 @@ import {
21
21
  import { isWelcomeVisible, resolveWelcomeConfig } from "../welcome";
22
22
  import { resolveContentMaxWidth } from "../utils/content-width";
23
23
 
24
+ /**
25
+ * Composer-bar chrome sizes. Not header controls: these float over the
26
+ * composer, so they opt out of the header control-size token with explicit
27
+ * per-control overrides. `CLOSE_ICON_SIZE` is the X's nominal size before the
28
+ * factory's sparse-viewBox compensation (10 x 1.4 = the rendered 14px).
29
+ */
30
+ export const COMPOSER_BAR_CONTROL_SIZE = "16px";
31
+ export const COMPOSER_BAR_CLOSE_ICON_SIZE = "10px";
32
+ export const COMPOSER_BAR_CLEAR_CHAT_ICON_SIZE = "14px";
33
+
24
34
  export interface PanelWrapper {
25
35
  wrapper: HTMLElement;
26
36
  panel: HTMLElement;
@@ -257,16 +267,16 @@ const buildComposerBarPanel = (
257
267
  // Minimal header: just an absolutely-positioned close button.
258
268
  // The wrapper uses inline styles (top/right/z-index values) because the
259
269
  // widget's hand-authored CSS doesn't ship every Tailwind utility.
260
- // Composer-bar's defaults are roughly half the floating-launcher's
261
- // (button 16px, icon 14px) to match the minimal aesthetic; the user can
262
- // still override via `launcher.closeButtonSize`.
270
+ // Composer-bar's close is a 16px box, far below the header control token,
271
+ // to match the minimal aesthetic; the user can still override via
272
+ // `launcher.closeButtonSize`.
263
273
  const { button: closeButton, wrapper: closeButtonWrapper } = createCloseButton(
264
274
  config,
265
275
  {
266
276
  showClose,
267
277
  wrapperClassName: "persona-composer-bar-close",
268
- buttonSize: "16px",
269
- iconSize: "14px",
278
+ buttonSize: COMPOSER_BAR_CONTROL_SIZE,
279
+ iconSize: COMPOSER_BAR_CLOSE_ICON_SIZE,
270
280
  }
271
281
  );
272
282
  closeButtonWrapper.style.position = "absolute";
@@ -288,8 +298,8 @@ const buildComposerBarPanel = (
288
298
  if (clearChatEnabled) {
289
299
  const parts = createClearChatButton(config, {
290
300
  wrapperClassName: "persona-composer-bar-clear-chat",
291
- buttonSize: "16px",
292
- iconSize: "14px",
301
+ buttonSize: COMPOSER_BAR_CONTROL_SIZE,
302
+ iconSize: COMPOSER_BAR_CLEAR_CHAT_ICON_SIZE,
293
303
  });
294
304
  clearChatButton = parts.button;
295
305
  clearChatButtonWrapper = parts.wrapper;
@@ -285,12 +285,79 @@ export const animateWelcomeOut = (
285
285
  onFinished(null);
286
286
  return null;
287
287
  }
288
+ // Collapse layout in the SAME motion as the fade. The default welcome is
289
+ // in-flow above the transcript (unlike plugin welcomes, which overlay it),
290
+ // so an opacity-only exit would hold the hero's height for the whole fade
291
+ // and then release it in one frame at the hidden flip — the first message
292
+ // renders below the hero, then teleports to the top when the repin lands.
293
+ //
294
+ // Height alone is not enough: the hero centers itself with auto margins,
295
+ // and auto margins absorb whatever the height collapse frees, so the
296
+ // transcript below would still not move until the flip. The margins'
297
+ // USED values are measured from geometry (computed style reports the
298
+ // keyword, not pixels, for flex auto margins) and animated to zero in the
299
+ // same keyframes, with a negative end margin canceling the host's flex gap
300
+ // slot — the transcript slides up continuously and the hidden flip at the
301
+ // end changes nothing the eye can see.
302
+ const rect = host.getBoundingClientRect();
303
+ const parent = host.parentElement;
304
+ let gap = 0;
305
+ let usedMarginTop = 0;
306
+ let usedMarginBottom = 0;
307
+ if (parent) {
308
+ const parentStyle = getComputedStyle(parent);
309
+ gap = parseFloat(parentStyle.rowGap) || 0;
310
+ const prev = host.previousElementSibling;
311
+ const next = host.nextElementSibling;
312
+ const parentRect = parent.getBoundingClientRect();
313
+ const contentTop =
314
+ parentRect.top +
315
+ (parseFloat(parentStyle.borderTopWidth) || 0) +
316
+ (parseFloat(parentStyle.paddingTop) || 0) -
317
+ parent.scrollTop;
318
+ usedMarginTop = Math.max(
319
+ 0,
320
+ prev
321
+ ? rect.top - prev.getBoundingClientRect().bottom - gap
322
+ : rect.top - contentTop
323
+ );
324
+ usedMarginBottom = Math.max(
325
+ 0,
326
+ next ? next.getBoundingClientRect().top - rect.bottom - gap : 0
327
+ );
328
+ }
329
+ const previousOverflow = host.style.overflow;
330
+ host.style.overflow = "hidden";
331
+ // Padding collapses with the height: under border-box sizing a height of
332
+ // zero clamps at the vertical padding, which would leave a padding-tall
333
+ // remnant releasing in one frame at the flip.
334
+ const hostStyle = getComputedStyle(host);
335
+ const paddingTop = parseFloat(hostStyle.paddingTop) || 0;
336
+ const paddingBottom = parseFloat(hostStyle.paddingBottom) || 0;
288
337
  // `forwards`, not `backwards`: backwards only fills the delay phase, so the
289
338
  // element snaps back to opacity 1 for a frame after the keyframes end.
290
339
  const animation = host.animate(
291
340
  [
292
- { opacity: "1", transform: "none" },
293
- { opacity: "0", transform: "translateY(-4px)" },
341
+ {
342
+ opacity: "1",
343
+ transform: "none",
344
+ height: `${rect.height}px`,
345
+ minHeight: "0px",
346
+ paddingTop: `${paddingTop}px`,
347
+ paddingBottom: `${paddingBottom}px`,
348
+ marginTop: `${usedMarginTop}px`,
349
+ marginBottom: `${usedMarginBottom}px`,
350
+ },
351
+ {
352
+ opacity: "0",
353
+ transform: "translateY(-4px)",
354
+ height: "0px",
355
+ minHeight: "0px",
356
+ paddingTop: "0px",
357
+ paddingBottom: "0px",
358
+ marginTop: "0px",
359
+ marginBottom: `${-gap}px`,
360
+ },
294
361
  ],
295
362
  { duration: 180, easing: "ease-out", fill: "forwards" }
296
363
  );
@@ -298,6 +365,9 @@ export const animateWelcomeOut = (
298
365
  const settle = () => {
299
366
  if (settled) return;
300
367
  settled = true;
368
+ // Restored on BOTH settle paths (finish and a re-show's cancel), so a
369
+ // welcome brought back by clearChat keeps its natural overflow.
370
+ host.style.overflow = previousOverflow;
301
371
  onFinished(animation);
302
372
  };
303
373
  animation.finished.then(settle).catch(settle);
package/src/defaults.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { AgentWidgetConfig, AgentWidgetLauncherConfig } from "./types";
2
2
  import type { DeepPartial, PersonaTheme } from "./types/theme";
3
3
  import { deepMerge } from "./utils/deep-merge";
4
+ import {
5
+ DEFAULT_TOOLTIP_DELAY_MS,
6
+ DEFAULT_TOOLTIP_SKIP_DELAY_MS,
7
+ } from "./utils/tooltip-timing";
4
8
 
5
9
  /**
6
10
  * Default width for the floating launcher panel (when not overridden).
@@ -31,9 +35,10 @@ export const DEFAULT_LAUNCHER_CONFIG: AgentWidgetLauncherConfig = {
31
35
  callToActionIconHidden: false,
32
36
  agentIconSize: "40px",
33
37
  headerIconSize: "40px",
34
- closeButtonSize: "32px",
38
+ // closeButtonSize / clearChat.size omitted so theme.components.header.controlSize
39
+ // sizes the header controls; setting either here would pin them past the token.
35
40
  // Zero out browser-default <button> padding so the icon gets the full
36
- // 32x32 content box, matching clearChat.paddingX/Y below. Without this,
41
+ // content box, matching clearChat.paddingX/Y below. Without this,
37
42
  // UA stylesheets add ~1-2px vertical and ~6px horizontal padding that
38
43
  // eats into the border-box width and shrinks the rendered icon.
39
44
  closeButtonPaddingX: "0px",
@@ -52,7 +57,6 @@ export const DEFAULT_LAUNCHER_CONFIG: AgentWidgetLauncherConfig = {
52
57
  enabled: true,
53
58
  placement: "inline",
54
59
  iconName: "refresh-cw",
55
- size: "32px",
56
60
  showTooltip: true,
57
61
  tooltipText: "Clear chat",
58
62
  paddingX: "0px",
@@ -77,6 +81,10 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
77
81
  theme: undefined,
78
82
  darkTheme: undefined,
79
83
  colorScheme: "light",
84
+ tooltip: {
85
+ delayMs: DEFAULT_TOOLTIP_DELAY_MS,
86
+ skipDelayMs: DEFAULT_TOOLTIP_SKIP_DELAY_MS,
87
+ },
80
88
  launcher: DEFAULT_LAUNCHER_CONFIG,
81
89
  copy: {
82
90
  // No welcomeTitle / welcomeSubtitle here on purpose: `resolveWelcomeConfig`
@@ -168,6 +176,13 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
168
176
  freeTextPlaceholder: "Type your answer…",
169
177
  submitLabel: "Send",
170
178
  },
179
+ history: {
180
+ enabled: false,
181
+ presentation: "panel",
182
+ showScopeStatus: true,
183
+ showDelete: true,
184
+ showDeleteAll: true,
185
+ },
171
186
  },
172
187
  // Placeholder copy that models the right shape: verb-first, user-voice,
173
188
  // one line. Integrators should swap in domain prompts.
@@ -263,6 +278,10 @@ export function mergeWithDefaults(
263
278
  ...config.launcher?.clearChat,
264
279
  },
265
280
  },
281
+ tooltip: {
282
+ ...DEFAULT_WIDGET_CONFIG.tooltip,
283
+ ...config.tooltip,
284
+ },
266
285
  copy: {
267
286
  ...DEFAULT_WIDGET_CONFIG.copy,
268
287
  ...config.copy,
@@ -290,6 +309,8 @@ export function mergeWithDefaults(
290
309
  const csa = config.features?.streamAnimation;
291
310
  const dau = DEFAULT_WIDGET_CONFIG.features?.askUserQuestion;
292
311
  const cau = config.features?.askUserQuestion;
312
+ const dh = DEFAULT_WIDGET_CONFIG.features?.history;
313
+ const ch = config.features?.history;
293
314
  const mergedArtifacts =
294
315
  da === undefined && ca === undefined
295
316
  ? undefined
@@ -333,6 +354,17 @@ export function mergeWithDefaults(
333
354
  ...cau?.styles,
334
355
  },
335
356
  };
357
+ const mergedHistory =
358
+ dh === undefined && ch === undefined
359
+ ? undefined
360
+ : {
361
+ ...dh,
362
+ ...ch,
363
+ copy: {
364
+ ...dh?.copy,
365
+ ...ch?.copy,
366
+ },
367
+ };
336
368
  return {
337
369
  ...DEFAULT_WIDGET_CONFIG.features,
338
370
  ...config.features,
@@ -341,6 +373,7 @@ export function mergeWithDefaults(
341
373
  ...(mergedArtifacts !== undefined ? { artifacts: mergedArtifacts } : {}),
342
374
  ...(mergedStreamAnimation !== undefined ? { streamAnimation: mergedStreamAnimation } : {}),
343
375
  ...(mergedAskUserQuestion !== undefined ? { askUserQuestion: mergedAskUserQuestion } : {}),
376
+ ...(mergedHistory !== undefined ? { history: mergedHistory } : {}),
344
377
  };
345
378
  })(),
346
379
  suggestionChips: config.suggestionChips ?? DEFAULT_WIDGET_CONFIG.suggestionChips,
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ /**
6
+ * Bundle guard for the lazy event-stream-view chunk.
7
+ *
8
+ * The observability panel must NOT land in ANY core bundle shipped to
9
+ * consumers — neither the CDN IIFE (`dist/index.global.js`) nor the ESM/CJS
10
+ * bundles (`dist/index.js` / `dist/index.cjs`). It ships as the sibling chunk
11
+ * `dist/event-stream-view.{js,cjs}`, loaded on demand: the IIFE via a sibling
12
+ * URL, ESM/CJS via the external `@runtypelabs/persona/event-stream-view`
13
+ * subpath. A stray static import would pull the view back in; this fails then.
14
+ *
15
+ * Skips when `dist/` hasn't been built (e.g. a test-only CI step).
16
+ */
17
+ const dist = (f: string) => resolve(__dirname, "..", "dist", f);
18
+
19
+ // Chunk-only literals: the view's root and list class names are never emitted
20
+ // by core code; the sibling-URL stub in the core bundles is the bare filename
21
+ // `event-stream-view.js`, which does not contain these.
22
+ const RUNTIME_MARKERS = ["persona-event-stream-view", "persona-event-stream-list"];
23
+
24
+ const SUBPATH = "@runtypelabs/persona/event-stream-view";
25
+
26
+ describe("event-stream-view bundle split", () => {
27
+ const iifeBuilt =
28
+ existsSync(dist("index.global.js")) && existsSync(dist("event-stream-view.js"));
29
+ const esmBuilt = existsSync(dist("index.js")) && existsSync(dist("index.cjs"));
30
+
31
+ it.runIf(iifeBuilt)("keeps the event-stream view OUT of the core IIFE bundle", () => {
32
+ const core = readFileSync(dist("index.global.js"), "utf8");
33
+ for (const marker of RUNTIME_MARKERS) {
34
+ expect(core.includes(marker), `IIFE bundle unexpectedly contains "${marker}"`).toBe(
35
+ false
36
+ );
37
+ }
38
+ // The loader stub (sibling-URL reference) must remain so the chunk can load.
39
+ expect(core).toContain("event-stream-view.js");
40
+ });
41
+
42
+ it.runIf(esmBuilt)("keeps the event-stream view OUT of the ESM/CJS bundles", () => {
43
+ for (const file of ["index.js", "index.cjs"]) {
44
+ const core = readFileSync(dist(file), "utf8");
45
+ for (const marker of RUNTIME_MARKERS) {
46
+ expect(core.includes(marker), `${file} unexpectedly contains "${marker}"`).toBe(
47
+ false
48
+ );
49
+ }
50
+ // Any reference must go through the external subpath, not an inlined
51
+ // relative import.
52
+ expect(
53
+ core.includes(SUBPATH),
54
+ `${file} must reach the view via the external subpath`
55
+ ).toBe(true);
56
+ }
57
+ });
58
+
59
+ it.runIf(iifeBuilt)("ships the event-stream view in the sibling chunk", () => {
60
+ const chunk = readFileSync(dist("event-stream-view.js"), "utf8");
61
+ for (const marker of RUNTIME_MARKERS) {
62
+ expect(chunk.includes(marker), `chunk is missing "${marker}"`).toBe(true);
63
+ }
64
+ // A CJS twin exists for require()-based consumers.
65
+ expect(existsSync(dist("event-stream-view.cjs"))).toBe(true);
66
+ // Declarations ship with the chunk so the subpath is typed.
67
+ expect(existsSync(dist("event-stream-view.d.ts"))).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Event-stream-view runtime entry — the module that assembles the lazily
3
+ * loaded event-stream observability panel. It is NEVER statically imported on
4
+ * the core path: the IIFE/CDN build marks
5
+ * `@runtypelabs/persona/event-stream-view` external (`tsup.global.config.ts`)
6
+ * and loads the standalone `event-stream-view.js` chunk from a sibling URL on
7
+ * first open of the panel. The core reaches it through
8
+ * `event-stream-view-loader.ts`.
9
+ *
10
+ * Mirrors `history-view-entry.ts`.
11
+ */
12
+
13
+ export { createEventStreamView } from "./components/event-stream-view";
14
+ export type { EventStreamViewOptions } from "./components/event-stream-view";
15
+
16
+ import type { createEventStreamView as CreateEventStreamView } from "./components/event-stream-view";
17
+
18
+ /** Mounted-view handle: what `createEventStreamView` returns. */
19
+ export type EventStreamViewHandle = ReturnType<typeof CreateEventStreamView>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Loader indirection for the lazy event-stream-view chunk (core bundle, tiny).
3
+ *
4
+ * Mirrors `history-view-loader.ts`: the IIFE/CDN entry (`index-global.ts`)
5
+ * registers a loader that imports the standalone `event-stream-view.js` chunk
6
+ * from a sibling URL. ESM/CJS consumers fall back to importing the package's
7
+ * `./event-stream-view` subpath — which `build:client` marks external, so the
8
+ * view is NOT inlined into `dist/index.{js,cjs}` and stays out of the core
9
+ * bundle until the event-stream panel is first opened.
10
+ */
11
+
12
+ import { createChunkLoader } from "./utils/chunk-loader";
13
+ import type {
14
+ EventStreamViewOptions,
15
+ EventStreamViewHandle,
16
+ } from "./event-stream-view-entry";
17
+
18
+ export type EventStreamViewModule = {
19
+ createEventStreamView: (options: EventStreamViewOptions) => EventStreamViewHandle;
20
+ };
21
+
22
+ // IIFE/CDN: sibling-URL chunk via the registered loader.
23
+ // ESM/CJS fallback: the package's own `./event-stream-view` subpath (external,
24
+ // so the runtime chunk is code-split out of dist/index.{js,cjs} rather than
25
+ // inlined). Memoization + rejection-retry semantics live in `createChunkLoader`.
26
+ const { setLoader, load } = createChunkLoader<EventStreamViewModule>({
27
+ fallbackImport: () => import("@runtypelabs/persona/event-stream-view"),
28
+ });
29
+
30
+ export const setEventStreamViewLoader = setLoader;
31
+ export const loadEventStreamView = load;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Standalone event-stream-view chunk (`dist/event-stream-view.js`).
3
+ *
4
+ * The IIFE/CDN widget bundle marks `@runtypelabs/persona/event-stream-view`
5
+ * external so the observability panel is excluded from the core payload; this
6
+ * file is built as a self-contained sibling chunk
7
+ * (`tsup.event-stream-view.config.ts`) and loaded on demand by the loader
8
+ * registered in `index-global.ts`. ESM/CJS consumers reach it via this same
9
+ * (external) subpath.
10
+ *
11
+ * Lazy-runtime transport entry only: exports are limited to the mount contract
12
+ * the loader needs.
13
+ */
14
+
15
+ export { createEventStreamView } from "./event-stream-view-entry";
16
+ export type {
17
+ EventStreamViewOptions,
18
+ EventStreamViewHandle,
19
+ } from "./event-stream-view-entry";
@@ -535,6 +535,7 @@ export type RuntypeClientChatRequest = {
535
535
  export type RuntypeClientChatStreamEvent = RuntypeExecutionStreamEvent;
536
536
 
537
537
  export type RuntypeClientResumeRequest = {
538
+ assistantMessageId?: string;
538
539
  clientTools?: Array<{
539
540
  description: string;
540
541
  name: string;