@runtypelabs/persona 3.31.1 → 3.34.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 (51) hide show
  1. package/README.md +17 -10
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-quh7NmYD.d.cts → types-CthJFfNx.d.cts} +7 -0
  5. package/dist/animations/{types-quh7NmYD.d.ts → types-CthJFfNx.d.ts} +7 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/index.cjs +43 -43
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +320 -8
  13. package/dist/index.d.ts +320 -8
  14. package/dist/index.global.js +193 -192
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +43 -43
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js +117 -117
  19. package/dist/launcher.global.js.map +1 -1
  20. package/dist/smart-dom-reader.d.cts +110 -2
  21. package/dist/smart-dom-reader.d.ts +110 -2
  22. package/dist/theme-editor.cjs +30 -30
  23. package/dist/theme-editor.d.cts +124 -5
  24. package/dist/theme-editor.d.ts +124 -5
  25. package/dist/theme-editor.js +30 -30
  26. package/package.json +3 -3
  27. package/src/ask-user-question-tool.test.ts +148 -0
  28. package/src/ask-user-question-tool.ts +138 -0
  29. package/src/client.ts +43 -9
  30. package/src/codegen.test.ts +0 -14
  31. package/src/components/messages.ts +10 -0
  32. package/src/components/suggestions.ts +49 -6
  33. package/src/index-core.ts +15 -0
  34. package/src/runtime/host-layout.test.ts +206 -9
  35. package/src/runtime/host-layout.ts +121 -8
  36. package/src/runtime/init.test.ts +2 -2
  37. package/src/session.ts +188 -32
  38. package/src/session.webmcp.test.ts +48 -0
  39. package/src/suggest-replies-tool.test.ts +445 -0
  40. package/src/suggest-replies-tool.ts +152 -0
  41. package/src/theme-editor/index.ts +2 -0
  42. package/src/theme-editor/webmcp/index.ts +2 -0
  43. package/src/theme-editor/webmcp/types.ts +16 -3
  44. package/src/types.ts +108 -2
  45. package/src/ui.docked.test.ts +2 -2
  46. package/src/ui.suggest-replies.test.ts +237 -0
  47. package/src/ui.ts +57 -13
  48. package/src/utils/dock.test.ts +23 -1
  49. package/src/utils/dock.ts +2 -0
  50. package/src/voice/voice.test.ts +0 -51
  51. package/src/install-config.test.ts +0 -38
@@ -1,6 +1,6 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- import { afterEach, describe, expect, it } from "vitest";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
4
 
5
5
  import { createWidgetHostLayout } from "./host-layout";
6
6
 
@@ -108,7 +108,7 @@ describe("createWidgetHostLayout docked", () => {
108
108
  layout.destroy();
109
109
  });
110
110
 
111
- it("push reveal translates a track; main column width stays fixed in px", () => {
111
+ it("push reveal offsets a track with margin (no transform); main column width stays fixed in px", () => {
112
112
  const parent = document.createElement("div");
113
113
  parent.style.width = "800px";
114
114
  document.body.appendChild(parent);
@@ -132,15 +132,20 @@ describe("createWidgetHostLayout docked", () => {
132
132
  expect(shell.dataset.personaDockReveal).toBe("push");
133
133
  expect(contentSlot?.style.width).toBe("800px");
134
134
  expect(pushTrack?.style.width).toBe("1120px");
135
- expect(pushTrack?.style.transform).toBe("translateX(0)");
135
+ expect(pushTrack?.style.marginLeft).toBe("0px");
136
+ // The track must NOT carry a transform: a transformed ancestor becomes the
137
+ // containing block for `position: fixed` host chrome and pushes it
138
+ // off-screen (regression guard, see host-layout.ts push branch).
139
+ expect(pushTrack?.style.transform).toBe("");
136
140
 
137
141
  layout.syncWidgetState({ open: true, launcherEnabled: true });
138
- expect(pushTrack?.style.transform).toBe("translateX(-320px)");
142
+ expect(pushTrack?.style.marginLeft).toBe("-320px");
143
+ expect(pushTrack?.style.transform).toBe("");
139
144
 
140
145
  layout.destroy();
141
146
  });
142
147
 
143
- it("push reveal on the left uses negative translate when closed", () => {
148
+ it("push reveal on the left uses negative margin when closed", () => {
144
149
  const parent = document.createElement("div");
145
150
  parent.style.width = "600px";
146
151
  document.body.appendChild(parent);
@@ -159,10 +164,12 @@ describe("createWidgetHostLayout docked", () => {
159
164
  layout.updateConfig({ launcher: dockConfig });
160
165
 
161
166
  const pushTrack = shell.querySelector<HTMLElement>('[data-persona-dock-role="push-track"]');
162
- expect(pushTrack?.style.transform).toBe("translateX(-200px)");
167
+ expect(pushTrack?.style.marginLeft).toBe("-200px");
168
+ expect(pushTrack?.style.transform).toBe("");
163
169
 
164
170
  layout.syncWidgetState({ open: true, launcherEnabled: true });
165
- expect(pushTrack?.style.transform).toBe("translateX(0)");
171
+ expect(pushTrack?.style.marginLeft).toBe("0px");
172
+ expect(pushTrack?.style.transform).toBe("");
166
173
 
167
174
  layout.destroy();
168
175
  });
@@ -194,6 +201,161 @@ describe("createWidgetHostLayout docked", () => {
194
201
  layout.destroy();
195
202
  });
196
203
 
204
+ it("clamps the dock slot to the viewport guard and pins resize/emerge sticky", () => {
205
+ for (const reveal of ["resize", "emerge"] as const) {
206
+ const parent = document.createElement("div");
207
+ document.body.appendChild(parent);
208
+ const target = document.createElement("div");
209
+ parent.appendChild(target);
210
+
211
+ const layout = createWidgetHostLayout(target, {
212
+ launcher: {
213
+ mountMode: "docked",
214
+ autoExpand: false,
215
+ dock: { width: "320px", reveal },
216
+ },
217
+ });
218
+
219
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
220
+ expect(dockSlot?.style.maxHeight, reveal).not.toBe("");
221
+ expect(dockSlot?.style.position, reveal).toBe("sticky");
222
+ expect(dockSlot?.style.top, reveal).toBe("0px");
223
+
224
+ layout.destroy();
225
+ document.body.innerHTML = "";
226
+ }
227
+ });
228
+
229
+ it("clamps push and overlay dock slots without sticky (in-flow/absolute contexts)", () => {
230
+ // push: the slot is an in-flow `position: relative` column — max-height cap
231
+ // only, no sticky. overlay: keeps absolute positioning.
232
+ const cases = [
233
+ { reveal: "push" as const, position: "relative" },
234
+ { reveal: "overlay" as const, position: "absolute" },
235
+ ];
236
+ for (const { reveal, position } of cases) {
237
+ const parent = document.createElement("div");
238
+ document.body.appendChild(parent);
239
+ const target = document.createElement("div");
240
+ parent.appendChild(target);
241
+
242
+ const layout = createWidgetHostLayout(target, {
243
+ launcher: {
244
+ mountMode: "docked",
245
+ autoExpand: false,
246
+ dock: { width: "320px", reveal },
247
+ },
248
+ });
249
+
250
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
251
+ expect(dockSlot?.style.maxHeight, reveal).not.toBe("");
252
+ expect(dockSlot?.style.position, reveal).toBe(position);
253
+
254
+ layout.destroy();
255
+ document.body.innerHTML = "";
256
+ }
257
+ });
258
+
259
+ it("honors a custom dock.maxHeight and the false opt-out", () => {
260
+ const parent = document.createElement("div");
261
+ document.body.appendChild(parent);
262
+ const target = document.createElement("div");
263
+ parent.appendChild(target);
264
+
265
+ const dockConfig = {
266
+ mountMode: "docked" as const,
267
+ autoExpand: false,
268
+ dock: { width: "320px", maxHeight: "600px" },
269
+ };
270
+ const layout = createWidgetHostLayout(target, { launcher: dockConfig });
271
+
272
+ const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
273
+ expect(dockSlot?.style.maxHeight).toBe("600px");
274
+
275
+ layout.updateConfig({
276
+ launcher: { ...dockConfig, dock: { width: "320px", maxHeight: false } },
277
+ });
278
+ expect(dockSlot?.style.maxHeight).toBe("");
279
+ expect(dockSlot?.style.position).toBe("relative");
280
+ expect(dockSlot?.style.top).toBe("");
281
+
282
+ layout.destroy();
283
+ });
284
+
285
+ describe("height-chain warning", () => {
286
+ const withOffsetHeight = (impl: (el: HTMLElement) => number, fn: () => void): void => {
287
+ const original = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
288
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
289
+ configurable: true,
290
+ get(this: HTMLElement) {
291
+ return impl(this);
292
+ },
293
+ });
294
+ try {
295
+ fn();
296
+ } finally {
297
+ if (original) {
298
+ Object.defineProperty(HTMLElement.prototype, "offsetHeight", original);
299
+ }
300
+ }
301
+ };
302
+
303
+ const mountDocked = () => {
304
+ const parent = document.createElement("div");
305
+ document.body.appendChild(parent);
306
+ const target = document.createElement("div");
307
+ parent.appendChild(target);
308
+ return createWidgetHostLayout(target, {
309
+ launcher: {
310
+ mountMode: "docked",
311
+ autoExpand: true,
312
+ dock: { width: "320px" },
313
+ },
314
+ });
315
+ };
316
+
317
+ afterEach(() => {
318
+ vi.restoreAllMocks();
319
+ });
320
+
321
+ it("warns once when a percentage height does not resolve against the shell's parent", () => {
322
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
323
+ // Fixed-height probe measures (environment works); 100% probe collapses
324
+ // to 0 (no ancestor provides a definite height).
325
+ withOffsetHeight((el) => (el.style.height === "1px" ? 1 : 0), () => {
326
+ const layout = mountDocked();
327
+ layout.syncWidgetState({ open: false, launcherEnabled: true });
328
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
329
+ layout.destroy();
330
+ });
331
+ const heightWarnings = warn.mock.calls.filter((c) =>
332
+ String(c[0]).includes("definite height")
333
+ );
334
+ expect(heightWarnings).toHaveLength(1);
335
+ expect(String(heightWarnings[0][0])).toContain("100dvh");
336
+ });
337
+
338
+ it("does not warn when the height chain resolves", () => {
339
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
340
+ withOffsetHeight(() => 1, () => {
341
+ const layout = mountDocked();
342
+ layout.destroy();
343
+ });
344
+ expect(
345
+ warn.mock.calls.filter((c) => String(c[0]).includes("definite height"))
346
+ ).toHaveLength(0);
347
+ });
348
+
349
+ it("does not warn when the environment cannot measure layout (jsdom default)", () => {
350
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
351
+ const layout = mountDocked();
352
+ layout.destroy();
353
+ expect(
354
+ warn.mock.calls.filter((c) => String(c[0]).includes("definite height"))
355
+ ).toHaveLength(0);
356
+ });
357
+ });
358
+
197
359
  const withInnerWidth = (width: number, fn: () => void): void => {
198
360
  const prev = window.innerWidth;
199
361
  try {
@@ -235,6 +397,41 @@ describe("createWidgetHostLayout docked", () => {
235
397
  });
236
398
  });
237
399
 
400
+ it("resets the push track margin when a desktop push offset enters mobile fullscreen", () => {
401
+ const parent = document.createElement("div");
402
+ parent.style.width = "800px";
403
+ document.body.appendChild(parent);
404
+ const target = document.createElement("div");
405
+ parent.appendChild(target);
406
+
407
+ const layout = createWidgetHostLayout(target, {
408
+ launcher: {
409
+ mountMode: "docked",
410
+ autoExpand: false,
411
+ dock: { width: "320px", reveal: "push" },
412
+ },
413
+ });
414
+ const pushTrack = layout.shell?.querySelector<HTMLElement>(
415
+ '[data-persona-dock-role="push-track"]',
416
+ );
417
+
418
+ // Desktop, expanded: track carries a negative margin offset.
419
+ withInnerWidth(800, () => {
420
+ layout.syncWidgetState({ open: true, launcherEnabled: true });
421
+ });
422
+ expect(pushTrack?.style.marginLeft).toBe("-320px");
423
+
424
+ // Same layout falls into mobile fullscreen (viewport resize): the stale
425
+ // margin must be cleared or the width:100% track renders 320px off-screen.
426
+ withInnerWidth(500, () => {
427
+ window.dispatchEvent(new Event("resize"));
428
+ });
429
+ expect(layout.shell?.dataset.personaDockMobileFullscreen).toBe("true");
430
+ expect(pushTrack?.style.marginLeft).toBe("0px");
431
+
432
+ layout.destroy();
433
+ });
434
+
238
435
  it("does not use fixed fullscreen above mobile breakpoint", () => {
239
436
  withInnerWidth(800, () => {
240
437
  const parent = document.createElement("div");
@@ -277,7 +474,7 @@ describe("createWidgetHostLayout docked", () => {
277
474
 
278
475
  const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
279
476
  layout.syncWidgetState({ open: true, launcherEnabled: true });
280
- expect(dockSlot?.style.position).toBe("relative");
477
+ expect(dockSlot?.style.position).toBe("sticky");
281
478
  expect(layout.shell?.dataset.personaDockMobileFullscreen).toBeUndefined();
282
479
 
283
480
  layout.destroy();
@@ -325,7 +522,7 @@ describe("createWidgetHostLayout docked", () => {
325
522
 
326
523
  const dockSlot = layout.shell?.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
327
524
  layout.syncWidgetState({ open: false, launcherEnabled: true });
328
- expect(dockSlot?.style.position).toBe("relative");
525
+ expect(dockSlot?.style.position).toBe("sticky");
329
526
 
330
527
  layout.destroy();
331
528
  });
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AgentWidgetConfig,
3
+ AgentWidgetDockConfig,
3
4
  AgentWidgetStateSnapshot,
4
5
  } from "../types";
5
6
  import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
@@ -26,6 +27,84 @@ const parseDockWidthToPx = (width: string, shellClientWidth: number): number =>
26
27
  return 420;
27
28
  };
28
29
 
30
+ /**
31
+ * Viewport-overflow guard. The docked shell sizes itself with `height: 100%`,
32
+ * which only resolves when an ancestor provides a definite height; without one
33
+ * the dock column would grow with the conversation and scroll off the page.
34
+ * Clamping the slot keeps the panel viewport-sized (messages scroll
35
+ * internally) even when the page's height chain is missing. `100vh` is set
36
+ * first as a fallback for engines without `dvh` support — an invalid value
37
+ * leaves the previous one in place.
38
+ */
39
+ const applyDockSlotMaxHeight = (
40
+ dockSlot: HTMLElement,
41
+ maxHeight: string | false
42
+ ): void => {
43
+ if (maxHeight === false) {
44
+ dockSlot.style.maxHeight = "";
45
+ return;
46
+ }
47
+ dockSlot.style.maxHeight = "100vh";
48
+ dockSlot.style.maxHeight = maxHeight;
49
+ };
50
+
51
+ /**
52
+ * Sticky keeps the resize/emerge dock column pinned to the top of the viewport
53
+ * when the surrounding page is taller than the screen (e.g. a missing height
54
+ * chain or a deliberately scrolling page). With a properly sized shell it
55
+ * behaves exactly like the previous `position: relative`. Not used for push:
56
+ * push gets the max-height cap only, like overlay, since the dock slot is an
57
+ * in-flow `position: relative` column there rather than a viewport-pinned one.
58
+ * (The push track itself is offset with `margin-left`, not a transform, so it
59
+ * no longer establishes a containing block for fixed/sticky descendants.)
60
+ */
61
+ const applyInFlowDockSlotPosition = (
62
+ dockSlot: HTMLElement,
63
+ maxHeight: string | false
64
+ ): void => {
65
+ if (maxHeight === false) {
66
+ dockSlot.style.position = "relative";
67
+ dockSlot.style.top = "";
68
+ } else {
69
+ dockSlot.style.position = "sticky";
70
+ dockSlot.style.top = "0";
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Warns once per docked mount when no ancestor of the dock target provides a
76
+ * definite height, which is the common misconfiguration behind a dock panel
77
+ * that grows past the viewport. Probes with a throwaway `height: 100%` child
78
+ * of the shell's parent; a fixed-height reference probe first checks the
79
+ * environment can measure at all (display: none subtrees and jsdom measure
80
+ * everything as 0, and must not produce a false warning).
81
+ */
82
+ const warnIfDockHeightChainUnresolved = (
83
+ shell: HTMLElement,
84
+ dock: Required<AgentWidgetDockConfig>
85
+ ): void => {
86
+ const parent = shell.parentElement;
87
+ if (!parent) return;
88
+ const probe = shell.ownerDocument.createElement("div");
89
+ probe.style.cssText =
90
+ "width:0;height:1px;margin:0;padding:0;border:0;visibility:hidden;";
91
+ parent.appendChild(probe);
92
+ const measurable = probe.offsetHeight > 0;
93
+ probe.style.height = "100%";
94
+ const resolved = probe.offsetHeight > 0;
95
+ probe.remove();
96
+ if (!measurable || resolved) return;
97
+ console.warn(
98
+ "[AgentWidget] Docked mode: no ancestor of the dock target provides a definite height, " +
99
+ "so the dock panel cannot size to your layout." +
100
+ (dock.maxHeight === false
101
+ ? " The viewport guard is disabled (dock.maxHeight: false), so the panel will grow with the conversation and overflow the viewport."
102
+ : ` Falling back to clamping the panel to ${dock.maxHeight} (configurable via launcher.dock.maxHeight).`) +
103
+ " To size the panel from your layout instead, give the height chain a definite height " +
104
+ "(e.g. `html, body { height: 100% }`) down to the dock target's parent."
105
+ );
106
+ };
107
+
29
108
  const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): void => {
30
109
  const launcherEnabled = config?.launcher?.enabled ?? true;
31
110
  host.className = "persona-host";
@@ -53,6 +132,7 @@ const clearMobileFullscreenDockSlotStyles = (dockSlot: HTMLElement): void => {
53
132
  dockSlot.style.width = "";
54
133
  dockSlot.style.height = "";
55
134
  dockSlot.style.maxWidth = "";
135
+ dockSlot.style.maxHeight = "";
56
136
  dockSlot.style.minWidth = "";
57
137
  clearOverlayDockSlotStyles(dockSlot);
58
138
  };
@@ -72,6 +152,7 @@ const clearPushTrackStyles = (pushTrack: HTMLElement): void => {
72
152
  pushTrack.style.alignItems = "";
73
153
  pushTrack.style.transition = "";
74
154
  pushTrack.style.transform = "";
155
+ pushTrack.style.marginLeft = "";
75
156
  };
76
157
 
77
158
  const resetContentSlotFlexSizing = (contentSlot: HTMLElement): void => {
@@ -215,6 +296,11 @@ const applyDockStyles = (
215
296
  pushTrack.style.flex = "1 1 auto";
216
297
  pushTrack.style.alignItems = "stretch";
217
298
  pushTrack.style.transform = "none";
299
+ // Reset the desktop push offset: this fullscreen path applies styles
300
+ // inline without going through clearPushTrackStyles, so a stale negative
301
+ // marginLeft from a prior expanded desktop render would shift the now
302
+ // width:100% track off-screen on mobile.
303
+ pushTrack.style.marginLeft = "0";
218
304
  pushTrack.style.transition = "none";
219
305
  contentSlot.style.flex = "1 1 auto";
220
306
  contentSlot.style.width = "100%";
@@ -227,6 +313,7 @@ const applyDockStyles = (
227
313
 
228
314
  shell.removeAttribute("data-persona-dock-mobile-fullscreen");
229
315
  clearMobileFullscreenDockSlotStyles(dockSlot);
316
+ applyDockSlotMaxHeight(dockSlot, dock.maxHeight);
230
317
 
231
318
  if (dock.reveal === "overlay") {
232
319
  shell.style.display = "flex";
@@ -279,15 +366,24 @@ const applyDockStyles = (
279
366
 
280
367
  const panelPx = parseDockWidthToPx(dock.width, shell.clientWidth);
281
368
  const contentPx = Math.max(0, shell.clientWidth);
282
- const dockTransition = dock.animate ? "transform 180ms ease" : "none";
283
- const translate =
369
+ const dockTransition = dock.animate ? "margin-left 180ms ease" : "none";
370
+ // Slide the wide track with a negative left margin rather than a CSS
371
+ // transform. A `transform` (even `translateX(0)`) turns the push track into
372
+ // the containing block for any `position: fixed` descendant, so host pages
373
+ // that render viewport-fixed chrome inside the pushed content (e.g. the
374
+ // dashboard editor's `fixed top-0 right-0` toolbar) resolve `right: 0`
375
+ // against the track's right edge — `panelPx` past the viewport, off-screen.
376
+ // `margin-left` produces the identical visual offset (the track is clipped
377
+ // by the overflow:hidden shell) without establishing a containing block for
378
+ // fixed OR absolutely-positioned descendants.
379
+ const marginOffsetPx =
284
380
  dock.side === "right"
285
381
  ? expanded
286
- ? `translateX(-${panelPx}px)`
287
- : "translateX(0)"
382
+ ? -panelPx
383
+ : 0
288
384
  : expanded
289
- ? "translateX(0)"
290
- : `translateX(-${panelPx}px)`;
385
+ ? 0
386
+ : -panelPx;
291
387
 
292
388
  pushTrack.style.display = "flex";
293
389
  pushTrack.style.flexDirection = "row";
@@ -298,7 +394,10 @@ const applyDockStyles = (
298
394
  pushTrack.style.height = "100%";
299
395
  pushTrack.style.width = `${contentPx + panelPx}px`;
300
396
  pushTrack.style.transition = dockTransition;
301
- pushTrack.style.transform = translate;
397
+ pushTrack.style.marginLeft = `${marginOffsetPx}px`;
398
+ // Defensively clear any transform a previous reveal mode may have set —
399
+ // leaving one would re-establish the fixed-position containing block.
400
+ pushTrack.style.transform = "";
302
401
 
303
402
  contentSlot.style.flex = "0 0 auto";
304
403
  contentSlot.style.flexGrow = "0";
@@ -315,6 +414,7 @@ const applyDockStyles = (
315
414
  dockSlot.style.minWidth = dock.width;
316
415
  dockSlot.style.maxWidth = dock.width;
317
416
  dockSlot.style.position = "relative";
417
+ dockSlot.style.top = "";
318
418
  dockSlot.style.overflow = "hidden";
319
419
  dockSlot.style.transition = "none";
320
420
  dockSlot.style.pointerEvents = expanded ? "auto" : "none";
@@ -348,7 +448,7 @@ const applyDockStyles = (
348
448
  dockSlot.style.maxWidth = width;
349
449
  dockSlot.style.minWidth = width;
350
450
  dockSlot.style.minHeight = "0";
351
- dockSlot.style.position = "relative";
451
+ applyInFlowDockSlotPosition(dockSlot, dock.maxHeight);
352
452
  dockSlot.style.overflow =
353
453
  isEmerge ? "hidden" : collapsedClosed ? "hidden" : "visible";
354
454
  dockSlot.style.transition = dockTransition;
@@ -429,9 +529,22 @@ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): Wi
429
529
  resizeObserver.observe(shell);
430
530
  };
431
531
 
532
+ let heightChainChecked = false;
533
+
432
534
  const layout = (): void => {
433
535
  applyDockStyles(shell, pushTrack, contentSlot, dockSlot, host, config, expanded);
434
536
  syncPushResizeObserver();
537
+ // Check the height chain once, the first time the panel is actually shown
538
+ // in a layout that depends on it (mobile fullscreen is fixed-position and
539
+ // doesn't — keep checking until a dependent layout comes around).
540
+ if (
541
+ expanded &&
542
+ !heightChainChecked &&
543
+ shell.dataset.personaDockMobileFullscreen !== "true"
544
+ ) {
545
+ heightChainChecked = true;
546
+ warnIfDockHeightChainUnresolved(shell, resolveDockConfig(config));
547
+ }
435
548
  };
436
549
 
437
550
  const ownerWindow = shell.ownerDocument.defaultView;
@@ -424,11 +424,11 @@ describe("initAgentWidget docked mode", () => {
424
424
  const panelSlot = shell.querySelector<HTMLElement>('[data-persona-dock-role="panel"]')!;
425
425
  expect(pushTrack).not.toBeNull();
426
426
  expect(panelSlot.style.width).toBe("400px");
427
- expect(pushTrack?.style.transform).toBe("translateX(-400px)");
427
+ expect(pushTrack?.style.marginLeft).toBe("-400px");
428
428
 
429
429
  handle.close();
430
430
  expect(panelSlot.style.width).toBe("400px");
431
- expect(pushTrack?.style.transform).toBe("translateX(0)");
431
+ expect(pushTrack?.style.marginLeft).toBe("0px");
432
432
 
433
433
  handle.destroy();
434
434
  wrapper.remove();