@runtypelabs/persona 3.7.0 → 3.8.1

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.
@@ -214,6 +214,17 @@ interface MessageTokens {
214
214
  /** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
215
215
  shadow?: string;
216
216
  };
217
+ /** Border color between messages in the thread. */
218
+ border?: TokenReference<'color'>;
219
+ }
220
+ /** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
221
+ interface CollapsibleWidgetTokens {
222
+ /** Background for content areas. */
223
+ container?: TokenReference<'color'>;
224
+ /** Background for code blocks inside collapsible sections. */
225
+ surface?: TokenReference<'color'>;
226
+ /** Border color for collapsible sections. */
227
+ border?: TokenReference<'color'>;
217
228
  }
218
229
  interface MarkdownTokens {
219
230
  inlineCode: {
@@ -242,6 +253,27 @@ interface MarkdownTokens {
242
253
  fontWeight?: string;
243
254
  };
244
255
  };
256
+ /** Fenced code block styling. */
257
+ codeBlock?: {
258
+ background?: TokenReference<'color'>;
259
+ borderColor?: TokenReference<'color'>;
260
+ textColor?: TokenReference<'color'>;
261
+ };
262
+ /** Table styling. */
263
+ table?: {
264
+ headerBackground?: TokenReference<'color'>;
265
+ borderColor?: TokenReference<'color'>;
266
+ };
267
+ /** Horizontal rule styling. */
268
+ hr?: {
269
+ color?: TokenReference<'color'>;
270
+ };
271
+ /** Blockquote styling. */
272
+ blockquote?: {
273
+ borderColor?: TokenReference<'color'>;
274
+ background?: TokenReference<'color'>;
275
+ textColor?: TokenReference<'color'>;
276
+ };
245
277
  }
246
278
  interface VoiceTokens {
247
279
  recording: {
@@ -404,6 +436,8 @@ interface ComponentTokens {
404
436
  tab?: ArtifactTabTokens;
405
437
  pane?: ArtifactPaneTokens;
406
438
  };
439
+ /** Collapsible widget chrome (tool/reasoning/approval bubbles). */
440
+ collapsibleWidget?: CollapsibleWidgetTokens;
407
441
  }
408
442
  interface PaletteExtras {
409
443
  transitions?: Record<string, string>;
package/dist/widget.css CHANGED
@@ -834,6 +834,10 @@
834
834
  overflow-y: auto;
835
835
  }
836
836
 
837
+ .persona-widget-body {
838
+ overscroll-behavior: contain;
839
+ }
840
+
837
841
  .persona-overflow-hidden {
838
842
  overflow: hidden;
839
843
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
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",
@@ -24,7 +24,7 @@ function renderDefaultArtifactCard(
24
24
  root.className =
25
25
  "persona-flex persona-w-full persona-max-w-full persona-items-center persona-gap-3 persona-rounded-xl persona-px-4 persona-py-3";
26
26
  root.style.border = "1px solid var(--persona-border, #e5e7eb)";
27
- root.style.backgroundColor = "var(--persona-bg, #ffffff)";
27
+ root.style.backgroundColor = "var(--persona-surface, #ffffff)";
28
28
  root.style.cursor = "pointer";
29
29
  root.tabIndex = 0;
30
30
  root.setAttribute("role", "button");
@@ -1,6 +1,7 @@
1
1
  import { createElement, createElementInDocument } from "../utils/dom";
2
2
  import { renderLucideIcon } from "../utils/icons";
3
3
  import { AgentWidgetConfig } from "../types";
4
+ import { PORTALED_OVERLAY_Z_INDEX } from "../utils/constants";
4
5
 
5
6
  /** CSS `color` values; variables are set on `[data-persona-root]` from `theme.components.header`. */
6
7
  export const HEADER_THEME_CSS = {
@@ -233,6 +234,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
233
234
 
234
235
  // Position tooltip above button
235
236
  portaledTooltip.style.position = "fixed";
237
+ portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
236
238
  portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
237
239
  portaledTooltip.style.top = `${buttonRect.top - 8}px`;
238
240
  portaledTooltip.style.transform = "translate(-50%, -100%)";
@@ -389,6 +391,7 @@ export const buildHeader = (context: HeaderBuildContext): HeaderElements => {
389
391
 
390
392
  // Position tooltip above button
391
393
  portaledTooltip.style.position = "fixed";
394
+ portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
392
395
  portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
393
396
  portaledTooltip.style.top = `${buttonRect.top - 8}px`;
394
397
  portaledTooltip.style.transform = "translate(-50%, -100%)";
@@ -3,6 +3,7 @@ import { AgentWidgetConfig } from "../types";
3
3
  import { positionMap } from "../utils/positioning";
4
4
  import { isDockedMountMode } from "../utils/dock";
5
5
  import { renderLucideIcon } from "../utils/icons";
6
+ import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
6
7
 
7
8
  export interface LauncherButton {
8
9
  element: HTMLButtonElement;
@@ -174,12 +175,16 @@ export const createLauncherButton = (
174
175
  : positionMap["bottom-right"];
175
176
 
176
177
  const floatingBase =
177
- "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 persona-z-50";
178
+ "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";
178
179
  const dockedBase =
179
180
  "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";
180
181
 
181
182
  button.className = dockedMode ? dockedBase : `${floatingBase} ${positionClass}`;
182
-
183
+
184
+ if (!dockedMode) {
185
+ button.style.zIndex = String(launcher.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
186
+ }
187
+
183
188
  // Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
184
189
  const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
185
190
  const defaultShadow = "var(--persona-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
@@ -2,6 +2,7 @@ import { createElement } from "../utils/dom";
2
2
  import { AgentWidgetConfig } from "../types";
3
3
  import { positionMap } from "../utils/positioning";
4
4
  import { isDockedMountMode } from "../utils/dock";
5
+ import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
5
6
  import { buildHeader, attachHeaderToContainer, HeaderElements } from "./header-builder";
6
7
  import { buildHeaderWithLayout } from "./header-layouts";
7
8
  import { buildComposer, ComposerElements } from "./composer-builder";
@@ -58,8 +59,9 @@ export const createWrapper = (config?: AgentWidgetConfig): PanelWrapper => {
58
59
 
59
60
  const wrapper = createElement(
60
61
  "div",
61
- `persona-widget-wrapper persona-fixed ${position} persona-z-50 persona-transition`
62
+ `persona-widget-wrapper persona-fixed ${position} persona-transition`
62
63
  );
64
+ wrapper.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
63
65
 
64
66
  const panel = createElement(
65
67
  "div",
@@ -228,7 +228,7 @@ describe("createWidgetHostLayout docked", () => {
228
228
  const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
229
229
  layout.syncWidgetState({ open: true, launcherEnabled: true });
230
230
  expect(dockSlot?.style.position).toBe("fixed");
231
- expect(dockSlot?.style.zIndex).toBe("9999");
231
+ expect(dockSlot?.style.zIndex).toBe("100000");
232
232
  expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
233
233
 
234
234
  layout.destroy();
@@ -3,6 +3,7 @@ import type {
3
3
  AgentWidgetStateSnapshot,
4
4
  } from "../types";
5
5
  import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
6
+ import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
6
7
 
7
8
  export type WidgetHostLayoutMode = "direct" | "docked";
8
9
 
@@ -198,7 +199,7 @@ const applyDockStyles = (
198
199
  dockSlot.style.minWidth = "0";
199
200
  dockSlot.style.minHeight = "0";
200
201
  dockSlot.style.overflow = "hidden";
201
- dockSlot.style.zIndex = String(config?.launcher?.zIndex ?? 9999);
202
+ dockSlot.style.zIndex = String(config?.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
202
203
  dockSlot.style.transform = "none";
203
204
  dockSlot.style.transition = "none";
204
205
  dockSlot.style.pointerEvents = "auto";
@@ -834,6 +834,10 @@
834
834
  overflow-y: auto;
835
835
  }
836
836
 
837
+ .persona-widget-body {
838
+ overscroll-behavior: contain;
839
+ }
840
+
837
841
  .persona-overflow-hidden {
838
842
  overflow: hidden;
839
843
  }
@@ -233,6 +233,18 @@ export interface MessageTokens {
233
233
  /** Assistant bubble box-shadow (token ref or raw CSS, e.g. `none`). */
234
234
  shadow?: string;
235
235
  };
236
+ /** Border color between messages in the thread. */
237
+ border?: TokenReference<'color'>;
238
+ }
239
+
240
+ /** Collapsible widget chrome (tool bubbles, reasoning bubbles, approval bubbles). */
241
+ export interface CollapsibleWidgetTokens {
242
+ /** Background for content areas. */
243
+ container?: TokenReference<'color'>;
244
+ /** Background for code blocks inside collapsible sections. */
245
+ surface?: TokenReference<'color'>;
246
+ /** Border color for collapsible sections. */
247
+ border?: TokenReference<'color'>;
236
248
  }
237
249
 
238
250
  export interface MarkdownTokens {
@@ -262,6 +274,27 @@ export interface MarkdownTokens {
262
274
  fontWeight?: string;
263
275
  };
264
276
  };
277
+ /** Fenced code block styling. */
278
+ codeBlock?: {
279
+ background?: TokenReference<'color'>;
280
+ borderColor?: TokenReference<'color'>;
281
+ textColor?: TokenReference<'color'>;
282
+ };
283
+ /** Table styling. */
284
+ table?: {
285
+ headerBackground?: TokenReference<'color'>;
286
+ borderColor?: TokenReference<'color'>;
287
+ };
288
+ /** Horizontal rule styling. */
289
+ hr?: {
290
+ color?: TokenReference<'color'>;
291
+ };
292
+ /** Blockquote styling. */
293
+ blockquote?: {
294
+ borderColor?: TokenReference<'color'>;
295
+ background?: TokenReference<'color'>;
296
+ textColor?: TokenReference<'color'>;
297
+ };
265
298
  }
266
299
 
267
300
  export interface VoiceTokens {
@@ -438,6 +471,8 @@ export interface ComponentTokens {
438
471
  tab?: ArtifactTabTokens;
439
472
  pane?: ArtifactPaneTokens;
440
473
  };
474
+ /** Collapsible widget chrome (tool/reasoning/approval bubbles). */
475
+ collapsibleWidget?: CollapsibleWidgetTokens;
441
476
  }
442
477
 
443
478
  export interface PaletteExtras {
package/src/types.ts CHANGED
@@ -813,11 +813,16 @@ export type AgentWidgetLauncherConfig = {
813
813
  */
814
814
  mobileBreakpoint?: number;
815
815
  /**
816
- * CSS z-index applied to the widget wrapper when it is in a positioned mode
817
- * (floating panel, mobile fullscreen, or sidebar). Increase this value if
818
- * other elements on the host page appear on top of the widget.
816
+ * CSS z-index applied to the widget wrapper and launcher button in all
817
+ * positioned modes (floating panel, mobile fullscreen, sidebar, docked
818
+ * mobile fullscreen). Increase this value if other elements on the host
819
+ * page appear on top of the widget.
819
820
  *
820
- * @default 9999 in overlay modes (mobile fullscreen / sidebar); 50 for the regular floating panel
821
+ * In viewport-covering modes (sidebar, mobile fullscreen), the widget
822
+ * also elevates the host element's stacking context and locks
823
+ * document scroll to prevent background scrolling.
824
+ *
825
+ * @default 100000
821
826
  */
822
827
  zIndex?: number;
823
828
  callToActionIconText?: string;
@@ -39,7 +39,7 @@ describe("createAgentExperience overlay z-index", () => {
39
39
  const wrapper = mount.firstElementChild as HTMLElement | null;
40
40
 
41
41
  expect(wrapper).not.toBeNull();
42
- expect(wrapper?.style.zIndex).toBe("9999");
42
+ expect(wrapper?.style.zIndex).toBe("100000");
43
43
 
44
44
  controller.destroy();
45
45
  });
@@ -55,7 +55,39 @@ describe("createAgentExperience overlay z-index", () => {
55
55
  const wrapper = mount.firstElementChild as HTMLElement | null;
56
56
 
57
57
  expect(wrapper).not.toBeNull();
58
- expect(wrapper?.style.zIndex).toBe("9999");
58
+ expect(wrapper?.style.zIndex).toBe("100000");
59
+
60
+ controller.destroy();
61
+ });
62
+
63
+ it("defaults floating panel wrapper to the overlay z-index", () => {
64
+ setInnerWidth(1024);
65
+
66
+ const mount = createMount();
67
+ const controller = createAgentExperience(mount, {
68
+ apiUrl: "https://api.example.com/chat",
69
+ });
70
+
71
+ const wrapper = mount.firstElementChild as HTMLElement | null;
72
+
73
+ expect(wrapper).not.toBeNull();
74
+ expect(wrapper?.style.zIndex).toBe("100000");
75
+
76
+ controller.destroy();
77
+ });
78
+
79
+ it("respects a custom zIndex", () => {
80
+ const mount = createMount();
81
+ const controller = createAgentExperience(mount, {
82
+ apiUrl: "https://api.example.com/chat",
83
+ launcher: {
84
+ sidebarMode: true,
85
+ zIndex: 42,
86
+ },
87
+ });
88
+
89
+ const wrapper = mount.firstElementChild as HTMLElement | null;
90
+ expect(wrapper?.style.zIndex).toBe("42");
59
91
 
60
92
  controller.destroy();
61
93
  });
package/src/ui.ts CHANGED
@@ -40,7 +40,9 @@ import {
40
40
  resolveFollowStateFromScroll,
41
41
  resolveFollowStateFromWheel
42
42
  } from "./utils/auto-follow";
43
- import { statusCopy } from "./utils/constants";
43
+ import { statusCopy, DEFAULT_OVERLAY_Z_INDEX, PORTALED_OVERLAY_Z_INDEX } from "./utils/constants";
44
+ import { syncOverlayHostStacking } from "./utils/overlay-host-stacking";
45
+ import { acquireScrollLock } from "./utils/scroll-lock";
44
46
  import { isDockedMountMode, resolveDockConfig } from "./utils/dock";
45
47
  import { createLauncherButton } from "./components/launcher";
46
48
  import { createWrapper, buildPanel, buildHeader, buildComposer, attachHeaderToContainer } from "./components/panel";
@@ -1287,6 +1289,7 @@ export const createAgentExperience = (
1287
1289
  if (openPrevented === true) return;
1288
1290
  event.preventDefault();
1289
1291
  event.stopPropagation();
1292
+ artifactsPaneUserHidden = false;
1290
1293
  session.selectArtifact(artifactId);
1291
1294
  syncArtifactPane();
1292
1295
  });
@@ -1546,7 +1549,7 @@ export const createAgentExperience = (
1546
1549
  // Determine panel styling based on mode, with theme overrides
1547
1550
  const position = config.launcher?.position ?? 'bottom-left';
1548
1551
  const isLeftSidebar = position === 'bottom-left' || position === 'top-left';
1549
- const overlayZIndex = config.launcher?.zIndex ?? 9999;
1552
+ const overlayZIndex = config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX;
1550
1553
 
1551
1554
  // Default values based on mode
1552
1555
  let defaultPanelBorder = (sidebarMode || shouldGoFullscreen) ? 'none' : '1px solid var(--persona-border)';
@@ -1819,9 +1822,8 @@ export const createAgentExperience = (
1819
1822
  if (!isInlineEmbed && !dockedMode) {
1820
1823
  const maxHeightStyles = 'max-height: -moz-available !important; max-height: stretch !important;';
1821
1824
  const paddingStyles = sidebarMode ? '' : 'padding-top: 1.25em !important;';
1822
- // Override z-index only when explicitly configured; otherwise the persona-z-50 class applies
1823
- const zIndexStyles = !sidebarMode && config.launcher?.zIndex != null
1824
- ? `z-index: ${config.launcher.zIndex} !important;`
1825
+ const zIndexStyles = !sidebarMode
1826
+ ? `z-index: ${config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX} !important;`
1825
1827
  : '';
1826
1828
  wrapper.style.cssText += maxHeightStyles + paddingStyles + zIndexStyles;
1827
1829
  }
@@ -1834,6 +1836,16 @@ export const createAgentExperience = (
1834
1836
 
1835
1837
  const destroyCallbacks: Array<() => void> = [];
1836
1838
 
1839
+ let teardownHostStacking: (() => void) | null = null;
1840
+ let releaseScrollLock: (() => void) | null = null;
1841
+
1842
+ destroyCallbacks.push(() => {
1843
+ teardownHostStacking?.();
1844
+ teardownHostStacking = null;
1845
+ releaseScrollLock?.();
1846
+ releaseScrollLock = null;
1847
+ });
1848
+
1837
1849
  if (artifactPanelResizeObs) {
1838
1850
  destroyCallbacks.push(() => {
1839
1851
  artifactPanelResizeObs?.disconnect();
@@ -2033,7 +2045,8 @@ export const createAgentExperience = (
2033
2045
  container.appendChild(scrollToBottomButton);
2034
2046
  }
2035
2047
  updateScrollToBottomButtonOffset();
2036
- scrollToBottomButton.style.display = autoFollow.isFollowing() ? "none" : "";
2048
+ const hasOverflow = getScrollBottomOffset(body) > 0;
2049
+ scrollToBottomButton.style.display = (autoFollow.isFollowing() || !hasOverflow) ? "none" : "";
2037
2050
  };
2038
2051
 
2039
2052
  const pauseAutoScroll = () => {
@@ -2620,12 +2633,46 @@ export const createAgentExperience = (
2620
2633
  const prevOpen = open;
2621
2634
  open = nextOpen;
2622
2635
  updateOpenState();
2623
-
2636
+
2637
+ // Sync host stacking and scroll lock for viewport-covering modes
2638
+ const isViewportCovering = (() => {
2639
+ const sm = config.launcher?.sidebarMode ?? false;
2640
+ const ow = mount.ownerDocument.defaultView ?? window;
2641
+ const mf = config.launcher?.mobileFullscreen ?? true;
2642
+ const mb = config.launcher?.mobileBreakpoint ?? 640;
2643
+ const isMobile = ow.innerWidth <= mb;
2644
+ const dockedMF = isDockedMountMode(config) && mf && isMobile;
2645
+ return sm || (mf && isMobile && launcherEnabled) || dockedMF;
2646
+ })();
2647
+
2648
+ if (open && isViewportCovering) {
2649
+ if (!teardownHostStacking) {
2650
+ const root = mount.getRootNode();
2651
+ const hostEl = root instanceof ShadowRoot
2652
+ ? (root.host as HTMLElement)
2653
+ : mount.closest<HTMLElement>(".persona-host");
2654
+ if (hostEl) {
2655
+ teardownHostStacking = syncOverlayHostStacking(
2656
+ hostEl,
2657
+ config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
2658
+ );
2659
+ }
2660
+ }
2661
+ if (!releaseScrollLock) {
2662
+ releaseScrollLock = acquireScrollLock(mount.ownerDocument);
2663
+ }
2664
+ } else if (!open) {
2665
+ teardownHostStacking?.();
2666
+ teardownHostStacking = null;
2667
+ releaseScrollLock?.();
2668
+ releaseScrollLock = null;
2669
+ }
2670
+
2624
2671
  if (open) {
2625
2672
  recalcPanelHeight();
2626
2673
  scheduleAutoScroll(true);
2627
2674
  }
2628
-
2675
+
2629
2676
  // Emit widget state events
2630
2677
  const stateEvent: AgentWidgetStateEvent = {
2631
2678
  open,
@@ -3572,6 +3619,35 @@ export const createAgentExperience = (
3572
3619
  // overwrites updateOpenState()'s display:none when docked+closed. Re-sync after every recalc.
3573
3620
  updateScrollToBottomButtonOffset();
3574
3621
  updateOpenState();
3622
+
3623
+ // Sync scroll lock and host stacking when viewport mode changes (e.g. orientation change)
3624
+ if (open && launcherEnabled) {
3625
+ const ow = mount.ownerDocument.defaultView ?? window;
3626
+ const isMobile = ow.innerWidth <= (config.launcher?.mobileBreakpoint ?? 640);
3627
+ const sm = config.launcher?.sidebarMode ?? false;
3628
+ const mf = config.launcher?.mobileFullscreen ?? true;
3629
+ const dockedMF = isDockedMountMode(config) && mf && isMobile;
3630
+ const isVC = sm || (mf && isMobile && launcherEnabled) || dockedMF;
3631
+
3632
+ if (isVC && !releaseScrollLock) {
3633
+ const root = mount.getRootNode();
3634
+ const hostEl = root instanceof ShadowRoot
3635
+ ? (root.host as HTMLElement)
3636
+ : mount.closest<HTMLElement>(".persona-host");
3637
+ if (hostEl && !teardownHostStacking) {
3638
+ teardownHostStacking = syncOverlayHostStacking(
3639
+ hostEl,
3640
+ config.launcher?.zIndex ?? DEFAULT_OVERLAY_Z_INDEX
3641
+ );
3642
+ }
3643
+ releaseScrollLock = acquireScrollLock(mount.ownerDocument);
3644
+ } else if (!isVC) {
3645
+ teardownHostStacking?.();
3646
+ teardownHostStacking = null;
3647
+ releaseScrollLock?.();
3648
+ releaseScrollLock = null;
3649
+ }
3650
+ }
3575
3651
  }
3576
3652
  };
3577
3653
 
@@ -3671,6 +3747,7 @@ export const createAgentExperience = (
3671
3747
  // Clear messages in session (this will trigger onMessagesChanged which re-renders)
3672
3748
  session.clearMessages();
3673
3749
  messageCache.clear();
3750
+ resumeAutoScroll();
3674
3751
 
3675
3752
  // Always clear the default localStorage key
3676
3753
  try {
@@ -4250,6 +4327,7 @@ export const createAgentExperience = (
4250
4327
 
4251
4328
  // Position tooltip above button
4252
4329
  portaledTooltip.style.position = "fixed";
4330
+ portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
4253
4331
  portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
4254
4332
  portaledTooltip.style.top = `${buttonRect.top - 8}px`;
4255
4333
  portaledTooltip.style.transform = "translate(-50%, -100%)";
@@ -4464,6 +4542,7 @@ export const createAgentExperience = (
4464
4542
 
4465
4543
  // Position tooltip above button
4466
4544
  portaledTooltip.style.position = "fixed";
4545
+ portaledTooltip.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
4467
4546
  portaledTooltip.style.left = `${buttonRect.left + buttonRect.width / 2}px`;
4468
4547
  portaledTooltip.style.top = `${buttonRect.top - 8}px`;
4469
4548
  portaledTooltip.style.transform = "translate(-50%, -100%)";
@@ -4994,6 +5073,7 @@ export const createAgentExperience = (
4994
5073
  artifactsPaneUserHidden = false;
4995
5074
  session.clearMessages();
4996
5075
  messageCache.clear();
5076
+ resumeAutoScroll();
4997
5077
 
4998
5078
  // Always clear the default localStorage key
4999
5079
  try {
@@ -7,6 +7,19 @@ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
7
7
  error: "Offline"
8
8
  };
9
9
 
10
+ /**
11
+ * Default z-index for widget overlays. Used for the floating panel, launcher
12
+ * button, sidebar, mobile fullscreen, and docked mobile fullscreen modes.
13
+ * Integrators can override via `launcher.zIndex`.
14
+ */
15
+ export const DEFAULT_OVERLAY_Z_INDEX = 100000;
16
+
17
+ /**
18
+ * Z-index for elements portaled to document.body (tooltips, dropdowns).
19
+ * Must be above the widget overlay so portaled UI is not clipped.
20
+ */
21
+ export const PORTALED_OVERLAY_Z_INDEX = DEFAULT_OVERLAY_Z_INDEX + 1;
22
+
10
23
 
11
24
 
12
25
 
@@ -1,5 +1,6 @@
1
1
  import { createElement } from "./dom";
2
2
  import { renderLucideIcon } from "./icons";
3
+ import { PORTALED_OVERLAY_Z_INDEX } from "./constants";
3
4
 
4
5
  export interface DropdownMenuItem {
5
6
  id: string;
@@ -73,7 +74,7 @@ export function createDropdownMenu(options: CreateDropdownOptions): DropdownMenu
73
74
  if (portal) {
74
75
  // Fixed positioning — menu is portaled outside the anchor's overflow context
75
76
  menu.style.position = "fixed";
76
- menu.style.zIndex = "10000";
77
+ menu.style.zIndex = String(PORTALED_OVERLAY_Z_INDEX);
77
78
  } else {
78
79
  // Absolute positioning — menu lives inside the anchor
79
80
  menu.style.position = "absolute";
@@ -0,0 +1,61 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { syncOverlayHostStacking } from "./overlay-host-stacking";
5
+
6
+ describe("syncOverlayHostStacking", () => {
7
+ afterEach(() => {
8
+ document.body.innerHTML = "";
9
+ });
10
+
11
+ it("sets position relative on a static element and restores on teardown", () => {
12
+ const host = document.createElement("div");
13
+ document.body.appendChild(host);
14
+
15
+ const teardown = syncOverlayHostStacking(host);
16
+ expect(host.style.position).toBe("relative");
17
+ expect(host.style.zIndex).toBe("100000");
18
+ expect(host.style.isolation).toBe("isolate");
19
+
20
+ teardown();
21
+ expect(host.style.position).toBe("");
22
+ expect(host.style.zIndex).toBe("");
23
+ expect(host.style.isolation).toBe("");
24
+ });
25
+
26
+ it("preserves existing positioned value", () => {
27
+ const host = document.createElement("div");
28
+ host.style.position = "absolute";
29
+ document.body.appendChild(host);
30
+
31
+ const teardown = syncOverlayHostStacking(host);
32
+ expect(host.style.position).toBe("absolute");
33
+ expect(host.style.zIndex).toBe("100000");
34
+
35
+ teardown();
36
+ expect(host.style.position).toBe("absolute");
37
+ expect(host.style.zIndex).toBe("");
38
+ });
39
+
40
+ it("accepts custom z-index", () => {
41
+ const host = document.createElement("div");
42
+ document.body.appendChild(host);
43
+
44
+ const teardown = syncOverlayHostStacking(host, 42);
45
+ expect(host.style.zIndex).toBe("42");
46
+
47
+ teardown();
48
+ });
49
+
50
+ it("restores previous inline z-index on teardown", () => {
51
+ const host = document.createElement("div");
52
+ host.style.zIndex = "5";
53
+ document.body.appendChild(host);
54
+
55
+ const teardown = syncOverlayHostStacking(host);
56
+ expect(host.style.zIndex).toBe("100000");
57
+
58
+ teardown();
59
+ expect(host.style.zIndex).toBe("5");
60
+ });
61
+ });
@@ -0,0 +1,38 @@
1
+ import { DEFAULT_OVERLAY_Z_INDEX } from "./constants";
2
+
3
+ /**
4
+ * Elevates the light-DOM host element's stacking context so viewport-covering
5
+ * overlays (sidebar, fullscreen) can escape parent stacking traps.
6
+ *
7
+ * - If the host has `position: static`, sets it to `relative` (required for
8
+ * `z-index` to take effect).
9
+ * - Applies `z-index` matching the overlay default.
10
+ * - Applies `isolation: isolate` to create a predictable stacking context.
11
+ *
12
+ * @returns A teardown function that restores only the properties that were changed.
13
+ */
14
+ export function syncOverlayHostStacking(
15
+ host: HTMLElement,
16
+ zIndex: number = DEFAULT_OVERLAY_Z_INDEX
17
+ ): () => void {
18
+ const originalPosition = host.style.position;
19
+ const originalZIndex = host.style.zIndex;
20
+ const originalIsolation = host.style.isolation;
21
+
22
+ const computed = getComputedStyle(host);
23
+ const positionWasSet = computed.position === "static" || computed.position === "";
24
+ if (positionWasSet) {
25
+ host.style.position = "relative";
26
+ }
27
+
28
+ host.style.zIndex = String(zIndex);
29
+ host.style.isolation = "isolate";
30
+
31
+ return () => {
32
+ if (positionWasSet) {
33
+ host.style.position = originalPosition;
34
+ }
35
+ host.style.zIndex = originalZIndex;
36
+ host.style.isolation = originalIsolation;
37
+ };
38
+ }