@matterfact/embed 0.7.0 → 0.9.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 (50) hide show
  1. package/README.md +171 -1
  2. package/dist/chunk-6EM7T2JV.js +816 -0
  3. package/dist/chunk-6EM7T2JV.js.map +1 -0
  4. package/dist/{chunk-4Q2ROXLR.js → chunk-BKKXHSYU.js} +2 -2
  5. package/dist/chunk-FEXG4LQJ.js +3 -0
  6. package/dist/chunk-FEXG4LQJ.js.map +7 -0
  7. package/dist/chunk-NWNMS34P.js +2 -0
  8. package/dist/chunk-URGQBG4I.js +800 -0
  9. package/dist/chunk-URGQBG4I.js.map +1 -0
  10. package/dist/context-ACFBWIFH.js +3 -0
  11. package/dist/{context-YX2KXFLI.js.map → context-ACFBWIFH.js.map} +1 -1
  12. package/dist/{context-WU2F5CBC.js → context-U2HJJN2S.js} +4 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +4 -4
  15. package/dist/index.cjs +1006 -159
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +192 -32
  18. package/dist/index.d.ts +192 -32
  19. package/dist/index.js +537 -130
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +1042 -165
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +81 -1
  24. package/dist/react.d.ts +81 -1
  25. package/dist/react.js +566 -132
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-JOGZWESK.js → snapshot-4GXT6PKZ.js} +2 -2
  28. package/dist/{snapshot-MUXE7KXX.js → snapshot-Y75SCGCM.js} +3 -3
  29. package/dist/{snapshot-MUXE7KXX.js.map → snapshot-Y75SCGCM.js.map} +1 -1
  30. package/examples/embed-demo/README.md +37 -0
  31. package/examples/embed-demo/src/App.tsx +55 -21
  32. package/examples/embed-demo/src/HoistDemo.tsx +390 -0
  33. package/examples/embed-demo/src/main.tsx +7 -0
  34. package/examples/embed-demo/src/mockXH.ts +492 -0
  35. package/examples/embed-demo/src/placement.tsx +43 -0
  36. package/examples/embed-demo/src/styles.css +147 -2
  37. package/examples/embed-demo/vite.config.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/chunk-7I37ZFAJ.js +0 -2
  40. package/dist/chunk-AXKXNYRT.js +0 -381
  41. package/dist/chunk-AXKXNYRT.js.map +0 -1
  42. package/dist/chunk-D7R6WVHG.js +0 -2
  43. package/dist/chunk-D7R6WVHG.js.map +0 -7
  44. package/dist/chunk-RS6RMZ77.js +0 -396
  45. package/dist/chunk-RS6RMZ77.js.map +0 -1
  46. package/dist/context-YX2KXFLI.js +0 -3
  47. /package/dist/{chunk-4Q2ROXLR.js.map → chunk-BKKXHSYU.js.map} +0 -0
  48. /package/dist/{chunk-7I37ZFAJ.js.map → chunk-NWNMS34P.js.map} +0 -0
  49. /package/dist/{context-WU2F5CBC.js.map → context-U2HJJN2S.js.map} +0 -0
  50. /package/dist/{snapshot-JOGZWESK.js.map → snapshot-4GXT6PKZ.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -18,7 +18,7 @@
18
18
  * shapes are right, "deeper integration" is a transport swap (postMessage ->
19
19
  * direct call), not a rewrite. So model the DOMAIN here, not the plumbing.
20
20
  */
21
- declare const PROTOCOL_VERSION = 1;
21
+ declare const PROTOCOL_VERSION = 3;
22
22
  /** Every message is namespaced so we never collide with the host page's own postMessage traffic. */
23
23
  declare const CHANNEL = "mf-embed";
24
24
  /**
@@ -58,6 +58,43 @@ interface PageEntity {
58
58
  text?: string;
59
59
  data?: Record<string, unknown>;
60
60
  }
61
+ /**
62
+ * How a route's content is classified, for the annotated site map.
63
+ *
64
+ * The host DECORATES the site map we auto-derive (see `SiteMapEntry`) so the agent
65
+ * knows which routes carry MATTERFACT content and of what kind — app-wide awareness
66
+ * it can navigate toward. This is CLASSIFICATION only: no per-instance ids, and never
67
+ * a capability token. The concrete artifact/document on the CURRENT page still arrives
68
+ * via `host.context` entities / `host.artifactGrants`, resolved per page.
69
+ *
70
+ * - `mf-artifact` — a co-embedded matterfact artifact (its slug); the token stays on
71
+ * the live iframe / grant, never here.
72
+ * - `mf-document` — a matterfact document (dossier/report), resolved to an MF_DOC_ID
73
+ * host-side per page.
74
+ * - `host-data` — the host's own data; not a matterfact entity.
75
+ */
76
+ type ContentClass = {
77
+ kind: 'mf-artifact';
78
+ slug: string;
79
+ } | {
80
+ kind: 'mf-document';
81
+ doctype: string;
82
+ } | {
83
+ kind: 'host-data';
84
+ };
85
+ /**
86
+ * One route in the annotated site map: the path pattern + label we derived, plus the
87
+ * host's content classification. `current` marks the route the user is on.
88
+ */
89
+ interface SiteMapEntry {
90
+ /** Route pattern or path, e.g. '/app/company/:ticker/dossier'. */
91
+ path: string;
92
+ /** Human label for the route ('Company Dossier'). */
93
+ label?: string;
94
+ /** The host's classification of this route's content, when it annotated it. */
95
+ content?: ContentClass;
96
+ current?: boolean;
97
+ }
61
98
  /** A co-embedded artifact's read-only capability, for the backend to materialize its
62
99
  * data. Backend-only — never rendered. `token` is the artifact's own embed token. */
63
100
  interface ArtifactGrant {
@@ -143,6 +180,13 @@ interface HostTool {
143
180
  * requires the user to confirm, every time — see the note on ToolCall.
144
181
  */
145
182
  readOnly?: boolean;
183
+ /**
184
+ * The tiered confirm policy, set by the host (e.g. `window.matterfact.hoist.actions`):
185
+ * `'required'` ⇒ the widget must show a confirm card and get the user's approval
186
+ * before every call; `'auto'` ⇒ it may call without asking. Absent for a tool that
187
+ * predates this policy (treat as `'required'` — the safer default).
188
+ */
189
+ confirm?: 'auto' | 'required';
146
190
  }
147
191
  type HostToWidget = {
148
192
  type: 'host.ready';
@@ -167,6 +211,17 @@ type HostToWidget = {
167
211
  } | {
168
212
  type: 'host.artifactGrants';
169
213
  grants: ArtifactGrant[];
214
+ }
215
+ /**
216
+ * The annotated site map — the app's routes with the host's content
217
+ * classification. Sent on mount and re-sent on navigation (a virtualized SPA can
218
+ * change what's routable). Distinct from the per-page `host.context`: this is the
219
+ * whole app's structure, so the agent can reason about and navigate toward MF
220
+ * content the user isn't currently looking at.
221
+ */
222
+ | {
223
+ type: 'host.sitemap';
224
+ sitemap: SiteMapEntry[];
170
225
  } | {
171
226
  type: 'host.tools';
172
227
  tools: HostTool[];
@@ -183,6 +238,23 @@ type HostToWidget = {
183
238
  } | {
184
239
  type: 'host.theme';
185
240
  mode: 'light' | 'dark';
241
+ }
242
+ /**
243
+ * Where the widget currently IS. Sent on mount and after anything that moves it
244
+ * (drag, snap, mode change, viewport resize).
245
+ *
246
+ * The widget cannot work this out for itself — it can't see the viewport, and the
247
+ * loader owns the box. Without it the widget renders its launcher against a stale
248
+ * guess: a launcher dragged to the left edge kept drawing its circle bottom-RIGHT
249
+ * inside a left-anchored box, so the circle jumped as soon as the box grew for the
250
+ * hover pill. `growth` is the direction the pill/menu/panel must open (toward the
251
+ * viewport centre); `mode` decides whether the launcher is a circle or an edge tab.
252
+ */
253
+ | {
254
+ type: 'host.geometry';
255
+ mode: 'float' | 'dock';
256
+ dockSide: 'left' | 'right';
257
+ growth: 'tl' | 'tr' | 'bl' | 'br';
186
258
  };
187
259
  /**
188
260
  * The agent asking the host to do something.
@@ -245,14 +317,57 @@ type WidgetToHost = {
245
317
  height: number;
246
318
  }
247
319
  /**
248
- * Maximize / restore. Expanded makes the iframe fill the viewport (with a small
249
- * margin) so the composer's `/` picker, mention previews, and the transcript have
250
- * room the corner popup can't give them. The loader owns the iframe size, so only
251
- * it can do this `widget.resize` (height-only) can't widen the popup.
320
+ * Dock / undock the open panel.
321
+ *
322
+ * Docked is a FULL-HEIGHT SIDE PANEL on the launcher's own side left corners dock
323
+ * to the left edge, right corners to the right, unless `setDockSide` overrides it.
324
+ * This replaces the old `setExpanded`, which was hardcoded to the right edge and so
325
+ * threw the panel across the screen whenever the launcher sat on the left.
326
+ *
327
+ * The loader owns the box, so only it can do this — `widget.resize` (height-only)
328
+ * can neither widen the panel nor move it to an edge.
329
+ */
330
+ | {
331
+ type: 'widget.setMode';
332
+ mode: 'float' | 'dock';
333
+ }
334
+ /** Which edge the tab and its panel live on. */
335
+ | {
336
+ type: 'widget.setDockSide';
337
+ side: 'left' | 'right';
338
+ }
339
+ /** Place the FLOATING launcher at a specific corner (the menu's snap picker). */
340
+ | {
341
+ type: 'widget.snapCorner';
342
+ corner: 'tl' | 'tr' | 'bl' | 'br';
343
+ }
344
+ /**
345
+ * Resize the COLLAPSED launcher's host box so the widget can paint outside the 56px
346
+ * circle — the hover pill (`{w:180,h:56}`) or the context menu (its own box). The
347
+ * iframe clips to the host box, so anything beside the circle is invisible until the
348
+ * host makes room. `{w:56,h:56}` returns to the bare circle. Ignored while open.
252
349
  */
253
350
  | {
254
- type: 'widget.setExpanded';
255
- expanded: boolean;
351
+ type: 'widget.setLauncherRegion';
352
+ w: number;
353
+ h: number;
354
+ }
355
+ /**
356
+ * User resize of the floating panel, via the grip on its inner corner.
357
+ *
358
+ * Same contract as the launcher drag below, and for the same reason: the press is
359
+ * captured by the iframe, so the host cannot see the moves. The widget tracks the
360
+ * gesture and sends SCREEN-coordinate deltas; the loader owns the sizing, the
361
+ * per-corner sign of the delta, and the clamp.
362
+ */
363
+ | {
364
+ type: 'widget.resizeStart';
365
+ } | {
366
+ type: 'widget.resizeMove';
367
+ dx: number;
368
+ dy: number;
369
+ } | {
370
+ type: 'widget.resizeEnd';
256
371
  }
257
372
  /**
258
373
  * Launcher drag. The widget tracks the gesture; the host only moves the element.
@@ -278,18 +393,7 @@ type WidgetToHost = {
278
393
  } | {
279
394
  type: 'widget.dragEnd';
280
395
  }
281
- /**
282
- * Grow/shrink the iframe to fit the launcher's context menu. The collapsed launcher is
283
- * only ~64px tall, so a menu rendered inside it would be clipped by the iframe's own
284
- * bounds — the host has to make room before it can be seen. `widget.resize` can't do
285
- * this: it's height-only AND ignored while closed.
286
- */
287
- | {
288
- type: 'widget.setMenu';
289
- open: boolean;
290
- height: number;
291
- }
292
- /** Put the launcher back in the default corner and forget the dragged position. */
396
+ /** Put the launcher back in the default corner and forget the saved size/dock. */
293
397
  | {
294
398
  type: 'widget.resetPos';
295
399
  }
@@ -300,6 +404,16 @@ type WidgetToHost = {
300
404
  /** The widget has no session; the host must run the hosted-login popup. */
301
405
  | {
302
406
  type: 'widget.needsAuth';
407
+ }
408
+ /**
409
+ * Deeplink the host to one of its OWN routes — the "open on page" control on a
410
+ * co-embedded document's side-panel viewer. The widget is a cross-origin iframe and
411
+ * can't navigate the top window itself, so it asks the loader, which resolves the
412
+ * href against the host location and refuses anything off the host's own origin.
413
+ */
414
+ | {
415
+ type: 'widget.navigate';
416
+ href: string;
303
417
  };
304
418
  /**
305
419
  * Everything on the wire is wrapped.
@@ -386,7 +500,7 @@ interface LoaderConfig {
386
500
  * own side panel, tool drawer or tab and wants the agent to live there.
387
501
  *
388
502
  * This inverts who owns the chrome. In corner mode the widget owns everything: it draws
389
- * its own launcher and it tells us how big to be (setOpen/setExpanded/resize). Inside
503
+ * its own launcher and it tells us how big to be (setOpen/setDocked/resize). Inside
390
504
  * someone else's panel, both are theirs — a widget that resized its host's drawer, or
391
505
  * drew a second floating bubble inside it, is just broken. So here we fill the
392
506
  * container, ignore the size messages, and the widget skips the launcher and renders
@@ -422,11 +536,31 @@ declare class EmbedHost {
422
536
  private queue;
423
537
  private ready;
424
538
  private open;
425
- private expanded;
426
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
427
- private pos;
428
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
429
- private dragBase;
539
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
540
+ private geo;
541
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
542
+ private resizeBase;
543
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
544
+ private dockBase;
545
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
546
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
547
+ * read the box back, which stops being the thing we're moving the moment it docks. */
548
+ private dragFrom;
549
+ private dragAt;
550
+ /** The painted box when the drag began — the edge test measures what you can SEE
551
+ * touching the screen edge, which is not the anchor once the panel is open. */
552
+ private dragBox;
553
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
554
+ private dragGrowth;
555
+ /**
556
+ * Has this drag been clear of the dock band yet?
557
+ *
558
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
559
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
560
+ * up docked it instantly, before it had moved anywhere. Docking now requires
561
+ * ENTERING the band rather than merely starting in it.
562
+ */
563
+ private dragLeftBand;
430
564
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
431
565
  private context;
432
566
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
@@ -450,6 +584,13 @@ declare class EmbedHost {
450
584
  * on it has plenty of frames.
451
585
  */
452
586
  private onMessage;
587
+ /**
588
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
589
+ * source-valid MessageEvent — this routes a message through the identical logic
590
+ * without weakening the origin/source checks above, which stay the only real door.
591
+ */
592
+ __testHandle(msg: WidgetToHost): void;
593
+ private handle;
453
594
  /**
454
595
  * Own a drag for its lifetime.
455
596
  *
@@ -462,12 +603,31 @@ declare class EmbedHost {
462
603
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
463
604
  * widget has to own the gesture. All we do is take a delta and place the element.
464
605
  */
465
- private cornerOffset;
466
- private dragTo;
467
- /** Reset to the default corner (a menu action the drag is otherwise sticky). */
468
- private resetPos;
469
- private readPos;
470
- private writePos;
606
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
607
+ private applyBox;
608
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
609
+ /**
610
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
611
+ *
612
+ * Every consumer has to agree on this, which is the bug that made the widget
613
+ * flip-flop under small movements near the viewport centre: the box was frozen but
614
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
615
+ * swapping which corner it drew against (and it animates that), twitching back and
616
+ * forth while the loader held perfectly still.
617
+ */
618
+ private growthNow;
619
+ private place;
620
+ /**
621
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
622
+ * without this it draws its launcher against a stale guess — which is what made the
623
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
624
+ */
625
+ private publishGeometry;
626
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
627
+ private themeMode;
628
+ private onViewportResize;
629
+ private readGeometry;
630
+ private writeGeometry;
471
631
  /**
472
632
  * Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
473
633
  * call it and hand the token straight to the widget via `host.auth` — no popup. The
@@ -501,4 +661,4 @@ declare class EmbedHost {
501
661
  */
502
662
  declare function mount(config: LoaderConfig): EmbedHost;
503
663
 
504
- export { type ActivityEvent, type ArtifactGrant, CHANNEL, type DomSnapshot, type ElementRef, EmbedHost, type Envelope, type FocusContext, type HostToWidget, type HostTool, type LoaderConfig, PROTOCOL_VERSION, type PageContext, type PageEntity, type ToolCall, type WidgetToHost, envelope, isEnvelope, mount, readConfig };
664
+ export { type ActivityEvent, type ArtifactGrant, CHANNEL, type ContentClass, type DomSnapshot, type ElementRef, EmbedHost, type Envelope, type FocusContext, type HostToWidget, type HostTool, type LoaderConfig, PROTOCOL_VERSION, type PageContext, type PageEntity, type SiteMapEntry, type ToolCall, type WidgetToHost, envelope, isEnvelope, mount, readConfig };
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * shapes are right, "deeper integration" is a transport swap (postMessage ->
19
19
  * direct call), not a rewrite. So model the DOMAIN here, not the plumbing.
20
20
  */
21
- declare const PROTOCOL_VERSION = 1;
21
+ declare const PROTOCOL_VERSION = 3;
22
22
  /** Every message is namespaced so we never collide with the host page's own postMessage traffic. */
23
23
  declare const CHANNEL = "mf-embed";
24
24
  /**
@@ -58,6 +58,43 @@ interface PageEntity {
58
58
  text?: string;
59
59
  data?: Record<string, unknown>;
60
60
  }
61
+ /**
62
+ * How a route's content is classified, for the annotated site map.
63
+ *
64
+ * The host DECORATES the site map we auto-derive (see `SiteMapEntry`) so the agent
65
+ * knows which routes carry MATTERFACT content and of what kind — app-wide awareness
66
+ * it can navigate toward. This is CLASSIFICATION only: no per-instance ids, and never
67
+ * a capability token. The concrete artifact/document on the CURRENT page still arrives
68
+ * via `host.context` entities / `host.artifactGrants`, resolved per page.
69
+ *
70
+ * - `mf-artifact` — a co-embedded matterfact artifact (its slug); the token stays on
71
+ * the live iframe / grant, never here.
72
+ * - `mf-document` — a matterfact document (dossier/report), resolved to an MF_DOC_ID
73
+ * host-side per page.
74
+ * - `host-data` — the host's own data; not a matterfact entity.
75
+ */
76
+ type ContentClass = {
77
+ kind: 'mf-artifact';
78
+ slug: string;
79
+ } | {
80
+ kind: 'mf-document';
81
+ doctype: string;
82
+ } | {
83
+ kind: 'host-data';
84
+ };
85
+ /**
86
+ * One route in the annotated site map: the path pattern + label we derived, plus the
87
+ * host's content classification. `current` marks the route the user is on.
88
+ */
89
+ interface SiteMapEntry {
90
+ /** Route pattern or path, e.g. '/app/company/:ticker/dossier'. */
91
+ path: string;
92
+ /** Human label for the route ('Company Dossier'). */
93
+ label?: string;
94
+ /** The host's classification of this route's content, when it annotated it. */
95
+ content?: ContentClass;
96
+ current?: boolean;
97
+ }
61
98
  /** A co-embedded artifact's read-only capability, for the backend to materialize its
62
99
  * data. Backend-only — never rendered. `token` is the artifact's own embed token. */
63
100
  interface ArtifactGrant {
@@ -143,6 +180,13 @@ interface HostTool {
143
180
  * requires the user to confirm, every time — see the note on ToolCall.
144
181
  */
145
182
  readOnly?: boolean;
183
+ /**
184
+ * The tiered confirm policy, set by the host (e.g. `window.matterfact.hoist.actions`):
185
+ * `'required'` ⇒ the widget must show a confirm card and get the user's approval
186
+ * before every call; `'auto'` ⇒ it may call without asking. Absent for a tool that
187
+ * predates this policy (treat as `'required'` — the safer default).
188
+ */
189
+ confirm?: 'auto' | 'required';
146
190
  }
147
191
  type HostToWidget = {
148
192
  type: 'host.ready';
@@ -167,6 +211,17 @@ type HostToWidget = {
167
211
  } | {
168
212
  type: 'host.artifactGrants';
169
213
  grants: ArtifactGrant[];
214
+ }
215
+ /**
216
+ * The annotated site map — the app's routes with the host's content
217
+ * classification. Sent on mount and re-sent on navigation (a virtualized SPA can
218
+ * change what's routable). Distinct from the per-page `host.context`: this is the
219
+ * whole app's structure, so the agent can reason about and navigate toward MF
220
+ * content the user isn't currently looking at.
221
+ */
222
+ | {
223
+ type: 'host.sitemap';
224
+ sitemap: SiteMapEntry[];
170
225
  } | {
171
226
  type: 'host.tools';
172
227
  tools: HostTool[];
@@ -183,6 +238,23 @@ type HostToWidget = {
183
238
  } | {
184
239
  type: 'host.theme';
185
240
  mode: 'light' | 'dark';
241
+ }
242
+ /**
243
+ * Where the widget currently IS. Sent on mount and after anything that moves it
244
+ * (drag, snap, mode change, viewport resize).
245
+ *
246
+ * The widget cannot work this out for itself — it can't see the viewport, and the
247
+ * loader owns the box. Without it the widget renders its launcher against a stale
248
+ * guess: a launcher dragged to the left edge kept drawing its circle bottom-RIGHT
249
+ * inside a left-anchored box, so the circle jumped as soon as the box grew for the
250
+ * hover pill. `growth` is the direction the pill/menu/panel must open (toward the
251
+ * viewport centre); `mode` decides whether the launcher is a circle or an edge tab.
252
+ */
253
+ | {
254
+ type: 'host.geometry';
255
+ mode: 'float' | 'dock';
256
+ dockSide: 'left' | 'right';
257
+ growth: 'tl' | 'tr' | 'bl' | 'br';
186
258
  };
187
259
  /**
188
260
  * The agent asking the host to do something.
@@ -245,14 +317,57 @@ type WidgetToHost = {
245
317
  height: number;
246
318
  }
247
319
  /**
248
- * Maximize / restore. Expanded makes the iframe fill the viewport (with a small
249
- * margin) so the composer's `/` picker, mention previews, and the transcript have
250
- * room the corner popup can't give them. The loader owns the iframe size, so only
251
- * it can do this `widget.resize` (height-only) can't widen the popup.
320
+ * Dock / undock the open panel.
321
+ *
322
+ * Docked is a FULL-HEIGHT SIDE PANEL on the launcher's own side left corners dock
323
+ * to the left edge, right corners to the right, unless `setDockSide` overrides it.
324
+ * This replaces the old `setExpanded`, which was hardcoded to the right edge and so
325
+ * threw the panel across the screen whenever the launcher sat on the left.
326
+ *
327
+ * The loader owns the box, so only it can do this — `widget.resize` (height-only)
328
+ * can neither widen the panel nor move it to an edge.
329
+ */
330
+ | {
331
+ type: 'widget.setMode';
332
+ mode: 'float' | 'dock';
333
+ }
334
+ /** Which edge the tab and its panel live on. */
335
+ | {
336
+ type: 'widget.setDockSide';
337
+ side: 'left' | 'right';
338
+ }
339
+ /** Place the FLOATING launcher at a specific corner (the menu's snap picker). */
340
+ | {
341
+ type: 'widget.snapCorner';
342
+ corner: 'tl' | 'tr' | 'bl' | 'br';
343
+ }
344
+ /**
345
+ * Resize the COLLAPSED launcher's host box so the widget can paint outside the 56px
346
+ * circle — the hover pill (`{w:180,h:56}`) or the context menu (its own box). The
347
+ * iframe clips to the host box, so anything beside the circle is invisible until the
348
+ * host makes room. `{w:56,h:56}` returns to the bare circle. Ignored while open.
252
349
  */
253
350
  | {
254
- type: 'widget.setExpanded';
255
- expanded: boolean;
351
+ type: 'widget.setLauncherRegion';
352
+ w: number;
353
+ h: number;
354
+ }
355
+ /**
356
+ * User resize of the floating panel, via the grip on its inner corner.
357
+ *
358
+ * Same contract as the launcher drag below, and for the same reason: the press is
359
+ * captured by the iframe, so the host cannot see the moves. The widget tracks the
360
+ * gesture and sends SCREEN-coordinate deltas; the loader owns the sizing, the
361
+ * per-corner sign of the delta, and the clamp.
362
+ */
363
+ | {
364
+ type: 'widget.resizeStart';
365
+ } | {
366
+ type: 'widget.resizeMove';
367
+ dx: number;
368
+ dy: number;
369
+ } | {
370
+ type: 'widget.resizeEnd';
256
371
  }
257
372
  /**
258
373
  * Launcher drag. The widget tracks the gesture; the host only moves the element.
@@ -278,18 +393,7 @@ type WidgetToHost = {
278
393
  } | {
279
394
  type: 'widget.dragEnd';
280
395
  }
281
- /**
282
- * Grow/shrink the iframe to fit the launcher's context menu. The collapsed launcher is
283
- * only ~64px tall, so a menu rendered inside it would be clipped by the iframe's own
284
- * bounds — the host has to make room before it can be seen. `widget.resize` can't do
285
- * this: it's height-only AND ignored while closed.
286
- */
287
- | {
288
- type: 'widget.setMenu';
289
- open: boolean;
290
- height: number;
291
- }
292
- /** Put the launcher back in the default corner and forget the dragged position. */
396
+ /** Put the launcher back in the default corner and forget the saved size/dock. */
293
397
  | {
294
398
  type: 'widget.resetPos';
295
399
  }
@@ -300,6 +404,16 @@ type WidgetToHost = {
300
404
  /** The widget has no session; the host must run the hosted-login popup. */
301
405
  | {
302
406
  type: 'widget.needsAuth';
407
+ }
408
+ /**
409
+ * Deeplink the host to one of its OWN routes — the "open on page" control on a
410
+ * co-embedded document's side-panel viewer. The widget is a cross-origin iframe and
411
+ * can't navigate the top window itself, so it asks the loader, which resolves the
412
+ * href against the host location and refuses anything off the host's own origin.
413
+ */
414
+ | {
415
+ type: 'widget.navigate';
416
+ href: string;
303
417
  };
304
418
  /**
305
419
  * Everything on the wire is wrapped.
@@ -386,7 +500,7 @@ interface LoaderConfig {
386
500
  * own side panel, tool drawer or tab and wants the agent to live there.
387
501
  *
388
502
  * This inverts who owns the chrome. In corner mode the widget owns everything: it draws
389
- * its own launcher and it tells us how big to be (setOpen/setExpanded/resize). Inside
503
+ * its own launcher and it tells us how big to be (setOpen/setDocked/resize). Inside
390
504
  * someone else's panel, both are theirs — a widget that resized its host's drawer, or
391
505
  * drew a second floating bubble inside it, is just broken. So here we fill the
392
506
  * container, ignore the size messages, and the widget skips the launcher and renders
@@ -422,11 +536,31 @@ declare class EmbedHost {
422
536
  private queue;
423
537
  private ready;
424
538
  private open;
425
- private expanded;
426
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
427
- private pos;
428
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
429
- private dragBase;
539
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
540
+ private geo;
541
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
542
+ private resizeBase;
543
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
544
+ private dockBase;
545
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
546
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
547
+ * read the box back, which stops being the thing we're moving the moment it docks. */
548
+ private dragFrom;
549
+ private dragAt;
550
+ /** The painted box when the drag began — the edge test measures what you can SEE
551
+ * touching the screen edge, which is not the anchor once the panel is open. */
552
+ private dragBox;
553
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
554
+ private dragGrowth;
555
+ /**
556
+ * Has this drag been clear of the dock band yet?
557
+ *
558
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
559
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
560
+ * up docked it instantly, before it had moved anywhere. Docking now requires
561
+ * ENTERING the band rather than merely starting in it.
562
+ */
563
+ private dragLeftBand;
430
564
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
431
565
  private context;
432
566
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
@@ -450,6 +584,13 @@ declare class EmbedHost {
450
584
  * on it has plenty of frames.
451
585
  */
452
586
  private onMessage;
587
+ /**
588
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
589
+ * source-valid MessageEvent — this routes a message through the identical logic
590
+ * without weakening the origin/source checks above, which stay the only real door.
591
+ */
592
+ __testHandle(msg: WidgetToHost): void;
593
+ private handle;
453
594
  /**
454
595
  * Own a drag for its lifetime.
455
596
  *
@@ -462,12 +603,31 @@ declare class EmbedHost {
462
603
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
463
604
  * widget has to own the gesture. All we do is take a delta and place the element.
464
605
  */
465
- private cornerOffset;
466
- private dragTo;
467
- /** Reset to the default corner (a menu action the drag is otherwise sticky). */
468
- private resetPos;
469
- private readPos;
470
- private writePos;
606
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
607
+ private applyBox;
608
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
609
+ /**
610
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
611
+ *
612
+ * Every consumer has to agree on this, which is the bug that made the widget
613
+ * flip-flop under small movements near the viewport centre: the box was frozen but
614
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
615
+ * swapping which corner it drew against (and it animates that), twitching back and
616
+ * forth while the loader held perfectly still.
617
+ */
618
+ private growthNow;
619
+ private place;
620
+ /**
621
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
622
+ * without this it draws its launcher against a stale guess — which is what made the
623
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
624
+ */
625
+ private publishGeometry;
626
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
627
+ private themeMode;
628
+ private onViewportResize;
629
+ private readGeometry;
630
+ private writeGeometry;
471
631
  /**
472
632
  * Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
473
633
  * call it and hand the token straight to the widget via `host.auth` — no popup. The
@@ -501,4 +661,4 @@ declare class EmbedHost {
501
661
  */
502
662
  declare function mount(config: LoaderConfig): EmbedHost;
503
663
 
504
- export { type ActivityEvent, type ArtifactGrant, CHANNEL, type DomSnapshot, type ElementRef, EmbedHost, type Envelope, type FocusContext, type HostToWidget, type HostTool, type LoaderConfig, PROTOCOL_VERSION, type PageContext, type PageEntity, type ToolCall, type WidgetToHost, envelope, isEnvelope, mount, readConfig };
664
+ export { type ActivityEvent, type ArtifactGrant, CHANNEL, type ContentClass, type DomSnapshot, type ElementRef, EmbedHost, type Envelope, type FocusContext, type HostToWidget, type HostTool, type LoaderConfig, PROTOCOL_VERSION, type PageContext, type PageEntity, type SiteMapEntry, type ToolCall, type WidgetToHost, envelope, isEnvelope, mount, readConfig };