@matterfact/embed 0.6.0 → 0.8.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 (52) hide show
  1. package/README.md +99 -1
  2. package/dist/chunk-PNSYFXXU.js +791 -0
  3. package/dist/chunk-PNSYFXXU.js.map +1 -0
  4. package/dist/chunk-R2ZEJARX.js +776 -0
  5. package/dist/chunk-R2ZEJARX.js.map +1 -0
  6. package/dist/{chunk-4Q2ROXLR.js → chunk-UD7CAQXV.js} +2 -2
  7. package/dist/{chunk-7I37ZFAJ.js → chunk-UQCETVRF.js} +2 -2
  8. package/dist/chunk-W52Q7G4J.js +3 -0
  9. package/dist/chunk-W52Q7G4J.js.map +7 -0
  10. package/dist/context-ARBB2XD6.js +3 -0
  11. package/dist/{context-YX2KXFLI.js.map → context-ARBB2XD6.js.map} +1 -1
  12. package/dist/{context-WU2F5CBC.js → context-MVGSYIMB.js} +2 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +4 -4
  15. package/dist/index.cjs +986 -159
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +140 -31
  18. package/dist/index.d.ts +140 -31
  19. package/dist/index.js +542 -130
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +1012 -165
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +33 -1
  24. package/dist/react.d.ts +33 -1
  25. package/dist/react.js +561 -132
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-MUXE7KXX.js → snapshot-GL4YBMXD.js} +3 -3
  28. package/dist/{snapshot-MUXE7KXX.js.map → snapshot-GL4YBMXD.js.map} +1 -1
  29. package/dist/{snapshot-JOGZWESK.js → snapshot-UGTXZVB6.js} +2 -2
  30. package/examples/embed-demo/.env.example +5 -1
  31. package/examples/embed-demo/README.md +37 -0
  32. package/examples/embed-demo/package.json +1 -1
  33. package/examples/embed-demo/src/App.tsx +82 -44
  34. package/examples/embed-demo/src/HoistDemo.tsx +390 -0
  35. package/examples/embed-demo/src/config.ts +34 -4
  36. package/examples/embed-demo/src/main.tsx +7 -0
  37. package/examples/embed-demo/src/mockXH.ts +492 -0
  38. package/examples/embed-demo/src/placement.tsx +43 -0
  39. package/examples/embed-demo/src/styles.css +147 -2
  40. package/examples/embed-demo/vite.config.ts +2 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-AXKXNYRT.js +0 -381
  43. package/dist/chunk-AXKXNYRT.js.map +0 -1
  44. package/dist/chunk-D7R6WVHG.js +0 -2
  45. package/dist/chunk-D7R6WVHG.js.map +0 -7
  46. package/dist/chunk-RS6RMZ77.js +0 -396
  47. package/dist/chunk-RS6RMZ77.js.map +0 -1
  48. package/dist/context-YX2KXFLI.js +0 -3
  49. /package/dist/{chunk-4Q2ROXLR.js.map → chunk-UD7CAQXV.js.map} +0 -0
  50. /package/dist/{chunk-7I37ZFAJ.js.map → chunk-UQCETVRF.js.map} +0 -0
  51. /package/dist/{context-WU2F5CBC.js.map → context-MVGSYIMB.js.map} +0 -0
  52. /package/dist/{snapshot-JOGZWESK.js.map → snapshot-UGTXZVB6.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -11,6 +11,398 @@ var __export = (target, all) => {
11
11
  for (var name in all)
12
12
  __defProp(target, name, { get: all[name], enumerable: true });
13
13
  };
14
+
15
+ // src/redact.ts
16
+ function redact(text) {
17
+ let out = text;
18
+ for (const re of PII) out = out.replace(re, "[redacted]");
19
+ return out;
20
+ }
21
+ var PII;
22
+ var init_redact = __esm({
23
+ "src/redact.ts"() {
24
+ PII = [
25
+ /\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
26
+ // email
27
+ /\b(?:\d[ -]?){13,19}\b/g,
28
+ // card-ish
29
+ /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
30
+ // ssn — dashed, spaced, OR bare 9 digits
31
+ /\b\d{9,}\b/g,
32
+ // long bare digit runs: account / MRN / routing numbers
33
+ /(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
34
+ // us phone
35
+ /\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
36
+ // stripe-style api keys
37
+ /\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
38
+ // jwt
39
+ ];
40
+ }
41
+ });
42
+
43
+ // src/adapters/hoist.ts
44
+ function detectHoist(read) {
45
+ let rt;
46
+ try {
47
+ rt = read();
48
+ } catch {
49
+ return null;
50
+ }
51
+ if (!rt) return null;
52
+ const worthwhile = !!rt.route.name || rt.grids.length > 0 || rt.charts.length > 0 || rt.nav.length > 0;
53
+ return worthwhile ? rt : null;
54
+ }
55
+ function quote(s, max = 120) {
56
+ const clean = redact(s.replace(/\s+/g, " ").trim());
57
+ const capped = clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
58
+ return `"${capped.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
59
+ }
60
+ function cell(value) {
61
+ const s = value == null ? "" : String(value);
62
+ return `- cell ${quote(s)}`;
63
+ }
64
+ function pushRow(row, cols, out, pad) {
65
+ out.push(`${pad}- row:`);
66
+ for (const c of cols) out.push(`${pad} ${cell(row[c.field])}`);
67
+ }
68
+ function renderGrid(grid, rowCap, out, depth) {
69
+ const pad = " ".repeat(depth);
70
+ out.push(`${pad}- table ${quote(grid.title || "grid")}:`);
71
+ out.push(`${pad} - rowgroup "columns":`);
72
+ for (const c of grid.columns)
73
+ out.push(`${pad} - columnheader ${quote(c.header || c.field)}`);
74
+ if (grid.selected.length) {
75
+ out.push(`${pad} - rowgroup "selected":`);
76
+ for (const r of grid.selected) pushRow(r, grid.columns, out, `${pad} `);
77
+ }
78
+ const shown = grid.rows.slice(0, Math.max(0, rowCap));
79
+ out.push(`${pad} - rowgroup "rows":`);
80
+ for (const r of shown) pushRow(r, grid.columns, out, `${pad} `);
81
+ out.push(
82
+ `${pad} - text: ${quote(`showing ${shown.length} of ${grid.totalCount} rows`)}`
83
+ );
84
+ }
85
+ function seriesSummary(name, data) {
86
+ if (!data.length) return `series ${name}: 0 points`;
87
+ let min = data[0];
88
+ let max = data[0];
89
+ for (const n of data) {
90
+ if (n < min) min = n;
91
+ if (n > max) max = n;
92
+ }
93
+ const last = data[data.length - 1];
94
+ return `series ${name}: ${data.length} points, min ${min}, max ${max}, last ${last}`;
95
+ }
96
+ function renderChart(chart, out, depth) {
97
+ const pad = " ".repeat(depth);
98
+ out.push(`${pad}- figure ${quote(chart.title || "chart")}:`);
99
+ for (const s of chart.series) {
100
+ out.push(`${pad} - text: ${quote(seriesSummary(s.name, s.data))}`);
101
+ }
102
+ }
103
+ function renderNav(groups, out, depth) {
104
+ const pad = " ".repeat(depth);
105
+ for (const g of groups) {
106
+ if (!g.items.length) continue;
107
+ out.push(`${pad}- navigation ${quote(g.label)}:`);
108
+ for (const d of g.items) {
109
+ const mark = d.current ? " [current]" : "";
110
+ out.push(`${pad} - link ${quote(d.label)}${mark} \u2192 ${redact(d.path)}`);
111
+ }
112
+ }
113
+ }
114
+ function flattenDestinations(rt) {
115
+ return rt.nav.flatMap((g) => g.items);
116
+ }
117
+ function buildNavigateTool(rt, policy) {
118
+ const destinations = flattenDestinations(rt);
119
+ const bySection = rt.nav.filter((g) => g.items.length).map((g) => `${g.label} (${g.items.map((d) => d.label).join(", ")})`).join("; ");
120
+ return {
121
+ name: "hoist.navigate",
122
+ description: redact(
123
+ `Navigate the user to a destination in the app. "destination" must be one of the destinations currently available, grouped by section: ${bySection || "(none available)"}.`
124
+ ),
125
+ inputSchema: {
126
+ type: "object",
127
+ properties: {
128
+ destination: {
129
+ type: "string",
130
+ enum: destinations.map((d) => d.label)
131
+ },
132
+ params: { type: "object" }
133
+ },
134
+ required: ["destination"]
135
+ },
136
+ readOnly: false,
137
+ // 'confirm' policy ⇒ the widget must show a confirm card every call; 'auto' ⇒ it
138
+ // may call without asking. See the confirm field's doc comment on HostTool.
139
+ confirm: policy === "auto" ? "auto" : "required"
140
+ };
141
+ }
142
+ function buildHoistContext(rt) {
143
+ const selected = rt.grids.reduce((n, g) => n + g.selected.length, 0);
144
+ const view = rt.route.name || rt.route.path || "this view";
145
+ const bits = [`Viewing ${view}`];
146
+ if (selected) bits.push(`${selected} selected`);
147
+ if (rt.grids.length) bits.push(`${rt.grids.length} grid(s)`);
148
+ if (rt.charts.length) bits.push(`${rt.charts.length} chart(s)`);
149
+ return {
150
+ route: rt.route.name ? redact(rt.route.name) : void 0,
151
+ description: redact(`${bits.join(", ")}.`)
152
+ };
153
+ }
154
+ function renderHoistSnapshot(rt, rowCap) {
155
+ const out = [];
156
+ renderNav(rt.nav, out, 0);
157
+ let truncated = false;
158
+ for (const g of rt.grids) {
159
+ if (g.rows.length > rowCap) truncated = true;
160
+ renderGrid(g, rowCap, out, 0);
161
+ }
162
+ for (const c of rt.charts) renderChart(c, out, 0);
163
+ return { yaml: out.join("\n"), truncated };
164
+ }
165
+ var init_hoist = __esm({
166
+ "src/adapters/hoist.ts"() {
167
+ init_redact();
168
+ }
169
+ });
170
+
171
+ // src/adapters/hoist-runtime.ts
172
+ function readHoistConfig(win) {
173
+ const cfg = win?.matterfact?.hoist;
174
+ const ex = cfg?.excludeModels;
175
+ return {
176
+ rows: typeof cfg?.rows === "number" && cfg.rows >= 0 ? cfg.rows : 50,
177
+ excludeModels: Array.isArray(ex) ? ex : []
178
+ };
179
+ }
180
+ function getModelsByClass(XH, name) {
181
+ const models = typeof XH?.getModels === "function" ? XH.getModels(name) : [];
182
+ return Array.isArray(models) ? models : [];
183
+ }
184
+ function className(m) {
185
+ return m?.constructor?.name ?? "";
186
+ }
187
+ function modelId(m) {
188
+ return String(m?.xhId ?? m?.id ?? m?.modelId ?? "");
189
+ }
190
+ function headerOf(c) {
191
+ return String(c?.headerName ?? c?.displayName ?? c?.field ?? c?.colId ?? "");
192
+ }
193
+ function collectHeaders(cols, prefix, out) {
194
+ for (const c of Array.isArray(cols) ? cols : []) {
195
+ const children = c?.children;
196
+ if (Array.isArray(children) && children.length) {
197
+ collectHeaders(children, [...prefix, headerOf(c)], out);
198
+ } else {
199
+ const id = String(c?.colId ?? c?.field ?? "");
200
+ if (id) out.set(id, [...prefix, headerOf(c)].filter(Boolean).join(" "));
201
+ }
202
+ }
203
+ }
204
+ function readColumns(g) {
205
+ const leaves = typeof g?.getVisibleLeafColumns === "function" ? g.getVisibleLeafColumns() : Array.isArray(g?.columns) ? g.columns : [];
206
+ const headers = /* @__PURE__ */ new Map();
207
+ collectHeaders(Array.isArray(g?.columns) ? g.columns : [], [], headers);
208
+ return (Array.isArray(leaves) ? leaves : []).map((c) => {
209
+ const field = String(c?.colId ?? c?.field ?? "");
210
+ return { field, header: headers.get(field) || headerOf(c) };
211
+ }).filter((c) => c.field);
212
+ }
213
+ function readRow(rec, cols) {
214
+ const row = {};
215
+ for (const c of cols)
216
+ row[c.field] = typeof rec?.get === "function" ? rec.get(c.field) : rec?.data?.[c.field];
217
+ return row;
218
+ }
219
+ function readGrid(g) {
220
+ const columns = readColumns(g);
221
+ const records = Array.isArray(g?.store?.records) ? g.store.records : [];
222
+ return {
223
+ id: modelId(g),
224
+ title: String(g?.title ?? ""),
225
+ columns,
226
+ selected: (Array.isArray(g?.selectedRecords) ? g.selectedRecords : []).map(
227
+ (r) => readRow(r, columns)
228
+ ),
229
+ rows: records.map((r) => readRow(r, columns)),
230
+ totalCount: typeof g?.store?.allCount === "number" ? g.store.allCount : records.length
231
+ };
232
+ }
233
+ function hasContent(g) {
234
+ return g.totalCount > 0 || g.selected.length > 0;
235
+ }
236
+ function readChart(c) {
237
+ const series = c?.series ?? c?.highchartsConfig?.series ?? [];
238
+ return {
239
+ id: modelId(c),
240
+ title: String(c?.title ?? c?.highchartsConfig?.title?.text ?? ""),
241
+ series: (Array.isArray(series) ? series : []).map((s) => ({
242
+ name: String(s?.name ?? ""),
243
+ data: (Array.isArray(s?.data) ? s.data : []).map((d) => typeof d === "number" ? d : Number(d?.y ?? d?.[1])).filter((n) => Number.isFinite(n))
244
+ }))
245
+ };
246
+ }
247
+ function humanize(seg) {
248
+ return seg.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
249
+ }
250
+ function childrenOf(node) {
251
+ const k = node?.children;
252
+ return k ? Array.isArray(k) ? k : Array.from(k) : [];
253
+ }
254
+ function routeIndex(XH, params) {
255
+ const router = XH?.router;
256
+ const root = router?.rootNode ?? router?.config?.rootNode;
257
+ const out = /* @__PURE__ */ new Map();
258
+ if (!router || typeof router.buildPath !== "function" || !root) return out;
259
+ const walk2 = (node, prefix) => {
260
+ for (const c of childrenOf(node)) {
261
+ const seg = String(c?.name ?? "");
262
+ if (!seg) continue;
263
+ const name = prefix ? `${prefix}.${seg}` : seg;
264
+ let path = "";
265
+ try {
266
+ path = router.buildPath(name, params);
267
+ } catch {
268
+ path = "";
269
+ }
270
+ if (path && !out.has(seg)) out.set(seg, { path, name });
271
+ walk2(c, name);
272
+ }
273
+ };
274
+ walk2(root, "");
275
+ return out;
276
+ }
277
+ function readSitemap(XH, excluded, origin) {
278
+ const params = XH?.routerState?.params ?? {};
279
+ const currentName = String(XH?.routerState?.name ?? "");
280
+ const routes = routeIndex(XH, params);
281
+ if (!routes.size) return [];
282
+ const groups = [];
283
+ for (const tc of getModelsByClass(XH, "TabContainerModel")) {
284
+ if (excluded(tc)) continue;
285
+ const tabs = Array.isArray(tc?.tabs) ? tc.tabs : [];
286
+ const activeTabId = String(tc?.activeTabId ?? "");
287
+ const items = [];
288
+ let companyScoped = false;
289
+ for (const t of tabs) {
290
+ const id = String(t?.id ?? "");
291
+ const r = routes.get(id);
292
+ if (!id || !r) continue;
293
+ if (r.name.includes(".company.ticker.")) companyScoped = true;
294
+ items.push({
295
+ // Redact HERE, at the source — not in a downstream renderer. This is the
296
+ // one place a Hoist tab TITLE (host-page text, so potentially PII-shaped)
297
+ // becomes a `HoistDestination.label`, and that label is what EVERY
298
+ // consumer uses verbatim: renderNav's YAML, buildNavigateTool's
299
+ // description AND its inputSchema.enum, and executeHostTool's
300
+ // `.find(d => d.label === destination)` lookup. Redacting once here keeps
301
+ // all of them consistent by construction — an enum built from an
302
+ // ALREADY-redacted label can never leak PII a sibling renderer scrubbed
303
+ // (see Important #1), and a destination arg matching the redacted string
304
+ // (the only thing the agent ever saw) still resolves.
305
+ label: redact(String(t?.title ?? "") || humanize(id)),
306
+ // ABSOLUTE, against the HOST page's origin — not the router's bare path.
307
+ // This path's only job is to be a link the agent can hand the user, and
308
+ // the agent's answer renders inside our widget iframe, which is on OUR
309
+ // origin: a bare "/research" there resolves to app.matterfact.com/research
310
+ // (a dead link to the wrong app), which is exactly what shipped before.
311
+ // The adapter runs in the host page, so its own origin is the right one.
312
+ path: origin && r.path.startsWith("/") ? origin + r.path : r.path,
313
+ // The fully-qualified route name — what hoist.navigate hands XH.navigate.
314
+ // Always present: `r` only exists here because routes.get(id) resolved.
315
+ name: r.name,
316
+ current: id === activeTabId || r.name === currentName
317
+ });
318
+ }
319
+ if (!items.length) continue;
320
+ const ticker = String(params.ticker ?? "");
321
+ const label = companyScoped ? ticker ? `Company \xB7 ${ticker}` : "Company" : "Site";
322
+ groups.push({ label, items });
323
+ }
324
+ return groups;
325
+ }
326
+ function readHoistRuntime(win = typeof window !== "undefined" ? window : void 0) {
327
+ const XH = win?.XH;
328
+ if (!XH) return null;
329
+ const { excludeModels } = readHoistConfig(win);
330
+ const excluded = (m) => excludeModels.includes(className(m)) || excludeModels.includes(modelId(m));
331
+ const rs = XH.routerState ?? {};
332
+ return {
333
+ route: {
334
+ name: String(rs.name ?? ""),
335
+ path: String(rs.path ?? ""),
336
+ params: rs.params ?? {}
337
+ },
338
+ grids: getModelsByClass(XH, "GridModel").filter((m) => !excluded(m)).map(readGrid).filter(hasContent),
339
+ charts: getModelsByClass(XH, "ChartModel").filter((m) => !excluded(m)).map(readChart).filter((c) => c.series.length > 0),
340
+ nav: readSitemap(
341
+ XH,
342
+ excluded,
343
+ String(
344
+ win?.location?.origin ?? ""
345
+ )
346
+ )
347
+ };
348
+ }
349
+ function readActionsConfig(win) {
350
+ const navigate = win?.matterfact?.hoist?.actions?.navigate;
351
+ return {
352
+ navigate: navigate === "confirm" || navigate === "auto" ? navigate : "off"
353
+ };
354
+ }
355
+ function advertiseTools(win = typeof window !== "undefined" ? window : void 0) {
356
+ try {
357
+ const XH = win?.XH;
358
+ if (!XH) return [];
359
+ const { navigate } = readActionsConfig(win);
360
+ if (navigate === "off") return [];
361
+ const rt = readHoistRuntime(win);
362
+ if (!rt) return [];
363
+ return [buildNavigateTool(rt, navigate)];
364
+ } catch {
365
+ return [];
366
+ }
367
+ }
368
+ function executeHostTool(call, win = typeof window !== "undefined" ? window : void 0) {
369
+ try {
370
+ if (call.name !== "hoist.navigate") {
371
+ return { ok: false, error: "unknown tool" };
372
+ }
373
+ if (readActionsConfig(win).navigate === "off") {
374
+ return { ok: false, error: "navigation is disabled" };
375
+ }
376
+ const destination = String(call.args?.destination ?? "");
377
+ const rt = readHoistRuntime(win);
378
+ const item = rt ? flattenDestinations(rt).find((d) => d.label === destination) : void 0;
379
+ if (!item) return { ok: false, error: "unknown destination" };
380
+ const XH = win?.XH;
381
+ const params = {
382
+ ...XH?.routerState?.params ?? {},
383
+ ...call.args?.params ?? {}
384
+ };
385
+ if (typeof XH?.navigate === "function") {
386
+ XH.navigate(item.name, params);
387
+ } else if (typeof XH?.router?.navigate === "function") {
388
+ XH.router.navigate(item.name, params);
389
+ } else {
390
+ return { ok: false, error: "navigation unavailable" };
391
+ }
392
+ return { ok: true, result: redact(`navigated to ${destination}`) };
393
+ } catch (e) {
394
+ return {
395
+ ok: false,
396
+ error: redact(e instanceof Error ? e.message : "navigate failed")
397
+ };
398
+ }
399
+ }
400
+ var init_hoist_runtime = __esm({
401
+ "src/adapters/hoist-runtime.ts"() {
402
+ init_hoist();
403
+ init_redact();
404
+ }
405
+ });
14
406
  function createStyleCache() {
15
407
  const cache = /* @__PURE__ */ new WeakMap();
16
408
  return ((el, pseudo) => {
@@ -260,7 +652,7 @@ function isHidden(el, style, layout) {
260
652
  if (el.hasAttribute("hidden") || el.hasAttribute("inert")) return true;
261
653
  return !isVisible(el, style, layout);
262
654
  }
263
- function childrenOf(el) {
655
+ function childrenOf2(el) {
264
656
  if (el.shadowRoot) return el.shadowRoot.childNodes;
265
657
  if (el.tagName === "SLOT") {
266
658
  const assigned = el.assignedNodes({ flatten: true });
@@ -269,7 +661,7 @@ function childrenOf(el) {
269
661
  return el.childNodes;
270
662
  }
271
663
  function visitChildren(el, parent, style, w) {
272
- const children = childrenOf(el);
664
+ const children = childrenOf2(el);
273
665
  for (let i = 0; i < children.length; i++) {
274
666
  const child = children[i];
275
667
  if (child) visitNode(child, parent, style, w);
@@ -390,17 +782,17 @@ function sanitize(text, max) {
390
782
  const clean = redact(collapsed);
391
783
  return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
392
784
  }
393
- function quote(text) {
785
+ function quote2(text) {
394
786
  return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
395
787
  }
396
788
  function render(children, depth, out) {
397
789
  const pad = " ".repeat(depth);
398
790
  for (const child of children) {
399
791
  if (typeof child === "string") {
400
- out.push(`${pad}- text: ${quote(sanitize(child, MAX_TEXT))}`);
792
+ out.push(`${pad}- text: ${quote2(sanitize(child, MAX_TEXT))}`);
401
793
  continue;
402
794
  }
403
- const name = child.name ? ` ${quote(sanitize(child.name, MAX_NAME))}` : "";
795
+ const name = child.name ? ` ${quote2(sanitize(child.name, MAX_NAME))}` : "";
404
796
  const head = `${pad}- ${child.role}${name}${child.props}`;
405
797
  if (child.children.length) {
406
798
  out.push(`${head}:`);
@@ -494,11 +886,6 @@ __export(context_exports, {
494
886
  start: () => start,
495
887
  stop: () => stop
496
888
  });
497
- function redact(text) {
498
- let out = text;
499
- for (const re of PII) out = out.replace(re, "[redacted]");
500
- return out;
501
- }
502
889
  function isPrivate(el) {
503
890
  if (el.closest("[data-mf-private]")) return true;
504
891
  const tag = el.tagName;
@@ -580,6 +967,8 @@ async function resolveDeclaredContext() {
580
967
  const c = await mf.getPageContext();
581
968
  if (c) return c;
582
969
  }
970
+ const hoist = detectHoist(() => readHoistRuntime());
971
+ if (hoist) return buildHoistContext(hoist);
583
972
  return mf?.context ?? {};
584
973
  }
585
974
  async function publishContext() {
@@ -682,6 +1071,10 @@ function watchNavigation() {
682
1071
  pushActivity({ type: "nav", summary: `navigated to ${url}` });
683
1072
  void publishContext();
684
1073
  publishArtifactGrants();
1074
+ send?.({
1075
+ type: "host.tools",
1076
+ tools: advertiseTools(typeof window !== "undefined" ? window : void 0)
1077
+ });
685
1078
  };
686
1079
  navFire = fire;
687
1080
  for (const name of ["pushState", "replaceState"]) {
@@ -737,6 +1130,20 @@ function scheduleFocus() {
737
1130
  }
738
1131
  async function sendSnapshot(emit) {
739
1132
  if (!pageContextOn) return;
1133
+ const hoist = detectHoist(() => readHoistRuntime());
1134
+ if (hoist) {
1135
+ const { rows } = readHoistConfig(
1136
+ typeof window !== "undefined" ? window : void 0
1137
+ );
1138
+ const { yaml: yaml2, truncated: truncated2 } = renderHoistSnapshot(hoist, rows);
1139
+ emit({
1140
+ type: "host.snapshot",
1141
+ snapshot: { yaml: redact(yaml2), seq: ++snapshotSeq, truncated: truncated2 }
1142
+ });
1143
+ lastFocus = { ...lastFocus, visibleRefs: [] };
1144
+ emit({ type: "host.focus", focus: lastFocus });
1145
+ return;
1146
+ }
740
1147
  const { snapshot: snapshot2 } = await loadSnapshotModule();
741
1148
  const { yaml, truncated, visibleRefs } = snapshot2();
742
1149
  emit({
@@ -762,11 +1169,25 @@ async function sendRegion(ref, emit) {
762
1169
  emit({ type: "host.region", ref, yaml: redact(yaml) });
763
1170
  }
764
1171
  async function callTool(call, emit) {
1172
+ if (!pageContextOn) {
1173
+ emit({
1174
+ type: "host.toolResult",
1175
+ callId: call.callId,
1176
+ ok: false,
1177
+ error: "host tools are disabled"
1178
+ });
1179
+ return;
1180
+ }
1181
+ const r = executeHostTool(
1182
+ call,
1183
+ typeof window !== "undefined" ? window : void 0
1184
+ );
765
1185
  emit({
766
1186
  type: "host.toolResult",
767
1187
  callId: call.callId,
768
- ok: false,
769
- error: "Host tools are read-only in this version; no action was taken."
1188
+ ok: r.ok,
1189
+ result: r.result,
1190
+ error: r.error
770
1191
  });
771
1192
  }
772
1193
  function stop() {
@@ -836,10 +1257,20 @@ function start(emit, origin, pageContext = true, provider) {
836
1257
  }
837
1258
  const theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
838
1259
  emit({ type: "host.theme", mode: theme });
1260
+ if (pageContextOn) {
1261
+ const tools = advertiseTools(
1262
+ typeof window !== "undefined" ? window : void 0
1263
+ );
1264
+ if (tools.length) emit({ type: "host.tools", tools });
1265
+ }
839
1266
  }
840
- var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, pageContextOn, contextProvider, navFire, origPushState, origReplaceState, snapshotModule, PII, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
1267
+ var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, pageContextOn, contextProvider, navFire, origPushState, origReplaceState, snapshotModule, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
841
1268
  var init_context = __esm({
842
1269
  "src/context.ts"() {
1270
+ init_redact();
1271
+ init_hoist();
1272
+ init_hoist_runtime();
1273
+ init_redact();
843
1274
  widgetOrigin = "";
844
1275
  MAX_ACTIVITY = 40;
845
1276
  send = null;
@@ -851,22 +1282,6 @@ var init_context = __esm({
851
1282
  origPushState = null;
852
1283
  origReplaceState = null;
853
1284
  snapshotModule = null;
854
- PII = [
855
- /\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
856
- // email
857
- /\b(?:\d[ -]?){13,19}\b/g,
858
- // card-ish
859
- /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
860
- // ssn — dashed, spaced, OR bare 9 digits
861
- /\b\d{9,}\b/g,
862
- // long bare digit runs: account / MRN / routing numbers
863
- /(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
864
- // us phone
865
- /\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
866
- // stripe-style api keys
867
- /\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
868
- // jwt
869
- ];
870
1285
  FORM_CONTROLS = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
871
1286
  ACTIONABLE = 'a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[role="menuitem"],[role="tab"],[role="option"],[role="checkbox"],[role="switch"],[role="radio"],[onclick],[tabindex]';
872
1287
  lastFocus = {};
@@ -876,7 +1291,7 @@ var init_context = __esm({
876
1291
  });
877
1292
 
878
1293
  // src/protocol.ts
879
- var PROTOCOL_VERSION = 1;
1294
+ var PROTOCOL_VERSION = 2;
880
1295
  var CHANNEL = "mf-embed";
881
1296
  function envelope(payload, id) {
882
1297
  return {
@@ -890,6 +1305,213 @@ function isEnvelope(data) {
890
1305
  return typeof data === "object" && data !== null && data.channel === CHANNEL;
891
1306
  }
892
1307
 
1308
+ // src/geometry.ts
1309
+ var MARGIN = 20;
1310
+ var LAUNCHER = 56;
1311
+ var SNAP_RADIUS = 96;
1312
+ var TAB_W = 44;
1313
+ var TAB_H = 132;
1314
+ var EDGE_DOCK = 64;
1315
+ var DEFAULT_FLOAT_W = 420;
1316
+ var DEFAULT_FLOAT_H = 640;
1317
+ var MIN_FLOAT_W = 320;
1318
+ var MIN_FLOAT_H = 420;
1319
+ var DEFAULT_DOCK_W = 600;
1320
+ var MIN_DOCK_W = 320;
1321
+ var CORNERS = ["tl", "tr", "bl", "br"];
1322
+ function defaultGeometry() {
1323
+ return {
1324
+ // Dock is the default: a side panel reads as part of the host app.
1325
+ mode: "dock",
1326
+ corner: "br",
1327
+ x: 0,
1328
+ y: 0,
1329
+ floatW: DEFAULT_FLOAT_W,
1330
+ floatH: DEFAULT_FLOAT_H,
1331
+ dockSide: "right",
1332
+ tabY: 0,
1333
+ // 0 => centre it (see tabTop)
1334
+ dockW: DEFAULT_DOCK_W
1335
+ };
1336
+ }
1337
+ function clamp(n, lo, hi) {
1338
+ return Math.min(Math.max(n, lo), hi);
1339
+ }
1340
+ function cornerOrigin(corner, w, h, vw, vh) {
1341
+ return {
1342
+ x: corner[1] === "l" ? MARGIN : vw - MARGIN - w,
1343
+ y: corner[0] === "t" ? MARGIN : vh - MARGIN - h
1344
+ };
1345
+ }
1346
+ function launcherOrigin(g, vw, vh) {
1347
+ if (g.corner) return cornerOrigin(g.corner, LAUNCHER, LAUNCHER, vw, vh);
1348
+ return {
1349
+ x: clamp(g.x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER)),
1350
+ y: clamp(g.y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER))
1351
+ };
1352
+ }
1353
+ function growthCorner(g, vw, vh) {
1354
+ if (g.corner) return g.corner;
1355
+ const o = launcherOrigin(g, vw, vh);
1356
+ return nearestCorner(o.x + LAUNCHER / 2, o.y + LAUNCHER / 2, vw, vh);
1357
+ }
1358
+ function nearestCorner(cx, cy, vw, vh) {
1359
+ return `${cy < vh / 2 ? "t" : "b"}${cx < vw / 2 ? "l" : "r"}`;
1360
+ }
1361
+ function snapOrFree(x, y, vw, vh) {
1362
+ const cx = clamp(x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER));
1363
+ const cy = clamp(y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER));
1364
+ for (const corner of CORNERS) {
1365
+ const o = cornerOrigin(corner, LAUNCHER, LAUNCHER, vw, vh);
1366
+ if (Math.hypot(cx - o.x, cy - o.y) <= SNAP_RADIUS) {
1367
+ return { corner, x: cx, y: cy };
1368
+ }
1369
+ }
1370
+ return { corner: null, x: cx, y: cy };
1371
+ }
1372
+ function edgeDock(x, w, vw) {
1373
+ if (x <= EDGE_DOCK) return "left";
1374
+ if (x + w >= vw - EDGE_DOCK) return "right";
1375
+ return null;
1376
+ }
1377
+ function dragAnchor(g, vw, vh) {
1378
+ if (g.mode === "dock") {
1379
+ return { x: g.dockSide === "left" ? 0 : vw - TAB_W, y: tabTop(g, vh) };
1380
+ }
1381
+ return launcherOrigin(g, vw, vh);
1382
+ }
1383
+ function visibleBox(g, open, vw, vh) {
1384
+ if (g.mode === "dock") {
1385
+ const w2 = open ? Math.min(Math.max(g.dockW, MIN_DOCK_W), Math.max(0, vw - 2 * MARGIN)) : TAB_W;
1386
+ return { x: g.dockSide === "left" ? 0 : vw - w2, w: w2 };
1387
+ }
1388
+ const o = launcherOrigin(g, vw, vh);
1389
+ if (!open) return { x: o.x, w: LAUNCHER };
1390
+ const { w } = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
1391
+ return {
1392
+ x: growthCorner(g, vw, vh)[1] === "r" ? o.x + LAUNCHER - w : o.x,
1393
+ w
1394
+ };
1395
+ }
1396
+ function placeDrop(anchorX, anchorY, boxLeft, boxW, vw, vh) {
1397
+ const snapped = snapOrFree(anchorX, anchorY, vw, vh);
1398
+ if (snapped.corner) return { mode: "float", ...snapped };
1399
+ const side = edgeDock(boxLeft, boxW, vw);
1400
+ if (side) {
1401
+ return {
1402
+ mode: "dock",
1403
+ dockSide: side,
1404
+ tabY: clamp(anchorY, 0, Math.max(0, vh - TAB_H))
1405
+ };
1406
+ }
1407
+ return { mode: "float", ...snapped };
1408
+ }
1409
+ function clampSize(w, h, vw, vh, minW, minH) {
1410
+ return {
1411
+ w: Math.min(Math.max(w, minW), Math.max(0, vw - 2 * MARGIN)),
1412
+ h: Math.min(Math.max(h, minH), Math.max(0, vh - 2 * MARGIN))
1413
+ };
1414
+ }
1415
+ function applyResizeDelta(corner, startW, startH, dx, dy) {
1416
+ const wSign = corner[1] === "r" ? -1 : 1;
1417
+ const hSign = corner[0] === "b" ? -1 : 1;
1418
+ return { w: startW + wSign * dx, h: startH + hSign * dy };
1419
+ }
1420
+ function applyDockResizeDelta(side, startW, dx) {
1421
+ return side === "right" ? startW - dx : startW + dx;
1422
+ }
1423
+ function parseGeometry(raw) {
1424
+ if (!raw) return null;
1425
+ try {
1426
+ const p = JSON.parse(raw);
1427
+ if (!p || p.mode !== "float" && p.mode !== "dock") return null;
1428
+ const d = defaultGeometry();
1429
+ const num = (v, dv) => typeof v === "number" ? v : dv;
1430
+ return {
1431
+ mode: p.mode,
1432
+ corner: CORNERS.includes(p.corner) ? p.corner : null,
1433
+ x: num(p.x, d.x),
1434
+ y: num(p.y, d.y),
1435
+ floatW: num(p.floatW, d.floatW),
1436
+ floatH: num(p.floatH, d.floatH),
1437
+ dockSide: p.dockSide === "left" ? "left" : "right",
1438
+ tabY: num(p.tabY, d.tabY),
1439
+ dockW: num(p.dockW, d.dockW)
1440
+ };
1441
+ } catch {
1442
+ return null;
1443
+ }
1444
+ }
1445
+ function serializeGeometry(g) {
1446
+ return JSON.stringify(g);
1447
+ }
1448
+ var px = (n) => `${n}px`;
1449
+ function boxAt(ox, oy, w, h, corner, vw, vh) {
1450
+ const right = corner[1] === "r";
1451
+ const bottom = corner[0] === "b";
1452
+ const rawX = right ? vw - (ox + LAUNCHER) : ox;
1453
+ const rawY = bottom ? vh - (oy + LAUNCHER) : oy;
1454
+ const x = clamp(rawX, MARGIN, Math.max(MARGIN, vw - MARGIN - w));
1455
+ const y = clamp(rawY, MARGIN, Math.max(MARGIN, vh - MARGIN - h));
1456
+ return {
1457
+ top: bottom ? "auto" : px(y),
1458
+ bottom: bottom ? px(y) : "auto",
1459
+ left: right ? "auto" : px(x),
1460
+ right: right ? px(x) : "auto",
1461
+ width: px(w),
1462
+ height: px(h)
1463
+ };
1464
+ }
1465
+ function tabTop(g, vh) {
1466
+ const max = Math.max(0, vh - TAB_H);
1467
+ return clamp(g.tabY || Math.round((vh - TAB_H) / 2), 0, max);
1468
+ }
1469
+ function tabBox(g, vw, vh) {
1470
+ const left = g.dockSide === "left";
1471
+ return {
1472
+ top: px(tabTop(g, vh)),
1473
+ bottom: "auto",
1474
+ left: left ? "0px" : "auto",
1475
+ right: left ? "auto" : "0px",
1476
+ width: px(TAB_W),
1477
+ height: px(TAB_H)
1478
+ };
1479
+ }
1480
+ function floatChrome(g, w, h, vw, vh, growth) {
1481
+ const o = launcherOrigin(g, vw, vh);
1482
+ return boxAt(o.x, o.y, w, h, growth ?? growthCorner(g, vw, vh), vw, vh);
1483
+ }
1484
+ function dockChrome(g, w, h, vw, vh) {
1485
+ const left = g.dockSide === "left";
1486
+ return {
1487
+ top: px(tabTop(g, vh)),
1488
+ bottom: "auto",
1489
+ left: left ? "0px" : "auto",
1490
+ right: left ? "auto" : "0px",
1491
+ width: px(w),
1492
+ height: px(h)
1493
+ };
1494
+ }
1495
+ function floatBox(g, vw, vh, growth) {
1496
+ const f = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
1497
+ return floatChrome(g, f.w, f.h, vw, vh, growth);
1498
+ }
1499
+ function dockBox(g, vw, _vh) {
1500
+ const w = Math.min(
1501
+ Math.max(g.dockW, MIN_DOCK_W),
1502
+ Math.max(0, vw - 2 * MARGIN)
1503
+ );
1504
+ const left = g.dockSide === "left";
1505
+ return {
1506
+ top: "0px",
1507
+ bottom: "0px",
1508
+ height: "auto",
1509
+ left: left ? "0px" : "auto",
1510
+ right: left ? "auto" : "0px",
1511
+ width: px(w)
1512
+ };
1513
+ }
1514
+
893
1515
  // src/loader.ts
894
1516
  var DEFAULT_ORIGIN = "https://app.matterfact.com";
895
1517
  function devRequested() {
@@ -923,6 +1545,10 @@ function readConfig() {
923
1545
  pageContext: pageContextAttr !== "off" && pageContextAttr !== "false" && pageContextAttr !== "0"
924
1546
  // No `data-dev` — see the `dev` field's doc comment: the URL trigger is the
925
1547
  // point for the script-tag path, so there is deliberately no script-tag knob here.
1548
+ // No `data-actions` either: the action policy lives entirely in the lazy chunk,
1549
+ // read straight off `window.matterfact.hoist.actions` by hoist-runtime.ts's
1550
+ // readActionsConfig() — see LoaderConfig's class doc and MatterfactAgentProps.actions
1551
+ // (react.tsx) for the one remaining way to SET that global programmatically.
926
1552
  };
927
1553
  }
928
1554
  var POS_KEY = "mf.embed.pos";
@@ -935,15 +1561,42 @@ var EmbedHost = class {
935
1561
  this.queue = [];
936
1562
  this.ready = false;
937
1563
  this.open = false;
938
- this.expanded = false;
939
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
940
- this.pos = null;
941
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
942
- this.dragBase = null;
1564
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
1565
+ this.geo = defaultGeometry();
1566
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
1567
+ this.resizeBase = null;
1568
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
1569
+ this.dockBase = DEFAULT_DOCK_W;
1570
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
1571
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
1572
+ * read the box back, which stops being the thing we're moving the moment it docks. */
1573
+ this.dragFrom = null;
1574
+ this.dragAt = { x: 0, y: 0 };
1575
+ /** The painted box when the drag began — the edge test measures what you can SEE
1576
+ * touching the screen edge, which is not the anchor once the panel is open. */
1577
+ this.dragBox = { x: 0, w: 0 };
1578
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
1579
+ this.dragGrowth = null;
1580
+ /**
1581
+ * Has this drag been clear of the dock band yet?
1582
+ *
1583
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
1584
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
1585
+ * up docked it instantly, before it had moved anywhere. Docking now requires
1586
+ * ENTERING the band rather than merely starting in it.
1587
+ */
1588
+ this.dragLeftBand = false;
943
1589
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
944
1590
  this.context = null;
945
1591
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
946
1592
  this.hostEl = null;
1593
+ /** Host-auth storm guard (see provideAuth). Counts getAuthToken calls; past a cap we stop
1594
+ * calling the provider, rather than joining the storm. This host instance OUTLIVES iframe
1595
+ * reloads, so — unlike anything in the widget — the counter survives the very reload loop
1596
+ * that drives the storm. (Kept tiny on purpose: this file has a hard per-page size budget;
1597
+ * the widget side carries the windowed/retry-after-settle half.) Terse name = fewer bytes
1598
+ * in the stub; it is the host-auth request count. */
1599
+ this.ac = 0;
947
1600
  /**
948
1601
  * Every message is checked twice, on every single message — not once at setup.
949
1602
  *
@@ -955,87 +1608,10 @@ var EmbedHost = class {
955
1608
  if (event.origin !== this.config.origin) return;
956
1609
  if (event.source !== this.iframe?.contentWindow) return;
957
1610
  if (!isEnvelope(event.data)) return;
958
- const msg = event.data.payload;
959
- switch (msg.type) {
960
- case "widget.ready":
961
- this.ready = true;
962
- this.send({
963
- type: "host.ready",
964
- protocol: PROTOCOL_VERSION,
965
- origin: location.origin
966
- });
967
- this.flush();
968
- if (this.inline) void this.loadContext();
969
- break;
970
- case "widget.setOpen":
971
- if (this.inline) break;
972
- this.open = msg.open;
973
- this.iframe?.setAttribute("data-open", String(msg.open));
974
- if (!msg.open) {
975
- this.expanded = false;
976
- this.iframe?.setAttribute("data-expanded", "false");
977
- }
978
- if (msg.open) void this.loadContext();
979
- break;
980
- case "widget.setExpanded":
981
- if (this.inline) break;
982
- this.expanded = msg.expanded;
983
- this.iframe?.setAttribute("data-expanded", String(msg.expanded));
984
- if (this.iframe) this.iframe.style.height = "";
985
- break;
986
- case "widget.resize":
987
- if (this.inline) break;
988
- if (this.iframe && this.open && !this.expanded) {
989
- this.iframe.style.height = `${Math.min(msg.height, window.innerHeight - 40)}px`;
990
- }
991
- break;
992
- case "widget.requestContext":
993
- void this.loadContext().then((m) => m.provideContext());
994
- break;
995
- case "widget.requestSnapshot":
996
- void this.loadContext().then((m) => m.sendSnapshot(this.send));
997
- break;
998
- case "widget.readRegion":
999
- void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
1000
- break;
1001
- case "widget.callTool":
1002
- void this.loadContext().then((m) => m.callTool(msg.call, this.send));
1003
- break;
1004
- // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
1005
- // doesn't draw one), and moving or hiding the host's own panel from inside it
1006
- // would be us redecorating their app. An older cached loader could still be told
1007
- // any of these by a newer widget, so they're guarded rather than assumed absent.
1008
- case "widget.dragStart":
1009
- if (this.inline) break;
1010
- this.dragBase = this.cornerOffset();
1011
- break;
1012
- case "widget.dragMove":
1013
- if (this.inline) break;
1014
- this.dragTo(msg.dx, msg.dy);
1015
- break;
1016
- case "widget.dragEnd":
1017
- if (this.inline) break;
1018
- this.dragBase = null;
1019
- this.writePos(this.pos);
1020
- break;
1021
- case "widget.setMenu":
1022
- if (this.inline) break;
1023
- if (this.iframe && !this.open) {
1024
- this.iframe.style.height = msg.open ? `${64 + msg.height}px` : "";
1025
- }
1026
- break;
1027
- case "widget.resetPos":
1028
- if (this.inline) break;
1029
- this.resetPos();
1030
- break;
1031
- case "widget.hide":
1032
- if (this.inline) break;
1033
- if (this.hostEl) this.hostEl.style.display = "none";
1034
- break;
1035
- case "widget.needsAuth":
1036
- void this.provideAuth();
1037
- break;
1038
- }
1611
+ this.handle(event.data.payload);
1612
+ };
1613
+ this.onViewportResize = () => {
1614
+ this.place();
1039
1615
  };
1040
1616
  this.send = (msg) => {
1041
1617
  if (!this.ready) {
@@ -1061,34 +1637,36 @@ var EmbedHost = class {
1061
1637
  ].join(";");
1062
1638
  this.config.container.appendChild(host);
1063
1639
  } else {
1064
- this.pos = this.readPos();
1640
+ this.geo = this.readGeometry();
1065
1641
  host.style.cssText = [
1066
1642
  "all: initial",
1067
1643
  "position: fixed",
1068
- `right: ${this.pos ? this.pos.right : 20}px`,
1069
- `bottom: ${this.pos ? this.pos.bottom : 20}px`,
1070
1644
  // Below the max so a host that genuinely needs to cover us (a modal, a cookie
1071
1645
  // banner they are legally obliged to show) still can.
1072
1646
  "z-index: 2147483000",
1073
- "contain: layout style"
1647
+ "contain: layout style",
1648
+ // The host element owns the BOX now (position AND size); the iframe fills it.
1649
+ // Transitioning the box is what makes the corner snap and the open/dock read as
1650
+ // motion rather than a jump. Suppressed during drag/resize (see those handlers).
1651
+ "transition: width .18s ease, height .18s ease, top .18s ease, right .18s ease, bottom .18s ease, left .18s ease"
1074
1652
  ].join(";");
1075
1653
  document.body.appendChild(host);
1076
1654
  }
1077
1655
  this.shadow = host.attachShadow({ mode: "closed" });
1078
1656
  const style = document.createElement("style");
1079
- style.textContent = ":host{all:initial}iframe{border:0;display:block;color-scheme:light dark;}" + (this.inline ? "iframe{width:100%;height:100%}" : (
1080
- // Hairline ring + soft ambient: the widget must read as a crisp card on
1081
- // ANY host background, including pure white where a shadow alone bleeds.
1082
- 'iframe{border-radius:12px;box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029;width:400px;height:64px;transition:height .18s ease,width .18s ease}iframe[data-open="true"]{width:420px;height:640px}iframe[data-open="true"][data-expanded="true"]{width:min(600px,calc(100vw - 40px));height:calc(100vh - 40px)}'
1657
+ style.textContent = ":host{all:initial}iframe{border:0;display:block;width:100%;height:100%;background:transparent}" + (this.inline ? "" : (
1658
+ // color-scheme only on the OPEN panel on a collapsed launcher it makes the
1659
+ // UA paint an opaque canvas base behind our shapes.
1660
+ ':host([data-mode="float"]) iframe,:host([data-mode="dock"]) iframe{color-scheme:light dark}:host([data-mode="float"]) iframe{border-radius:12px;box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029}:host([data-mode="dock"]) iframe{box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029}:host([data-mode="dock"][data-flush="right"]) iframe{border-radius:12px 0 0 12px}:host([data-mode="dock"][data-flush="left"]) iframe{border-radius:0 12px 12px 0}'
1083
1661
  ));
1084
1662
  this.shadow.appendChild(style);
1085
1663
  const iframe = document.createElement("iframe");
1086
1664
  iframe.title = "matterfact assistant";
1087
1665
  iframe.setAttribute(
1088
1666
  "sandbox",
1089
- "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
1667
+ "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
1090
1668
  );
1091
- iframe.setAttribute("allow", "microphone");
1669
+ iframe.setAttribute("allow", "microphone; clipboard-write");
1092
1670
  iframe.src = `${this.config.origin}/embed/chat?k=${encodeURIComponent(
1093
1671
  this.config.publishableKey
1094
1672
  )}&o=${encodeURIComponent(location.origin)}` + (this.config.surface ? `&s=${encodeURIComponent(this.config.surface)}` : "") + // Either trigger works: the URL param (no redeploy needed) OR the config's
@@ -1097,6 +1675,223 @@ var EmbedHost = class {
1097
1675
  this.iframe = iframe;
1098
1676
  this.shadow.appendChild(iframe);
1099
1677
  window.addEventListener("message", this.onMessage);
1678
+ if (!this.inline) {
1679
+ window.addEventListener("resize", this.onViewportResize);
1680
+ this.place();
1681
+ }
1682
+ }
1683
+ /**
1684
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
1685
+ * source-valid MessageEvent — this routes a message through the identical logic
1686
+ * without weakening the origin/source checks above, which stay the only real door.
1687
+ */
1688
+ __testHandle(msg) {
1689
+ this.handle(msg);
1690
+ }
1691
+ handle(msg) {
1692
+ switch (msg.type) {
1693
+ case "widget.ready":
1694
+ this.ready = true;
1695
+ this.send({
1696
+ type: "host.ready",
1697
+ protocol: PROTOCOL_VERSION,
1698
+ origin: location.origin
1699
+ });
1700
+ this.send({ type: "host.theme", mode: this.themeMode() });
1701
+ this.flush();
1702
+ if (this.inline) void this.loadContext();
1703
+ break;
1704
+ case "widget.setOpen":
1705
+ if (this.inline) break;
1706
+ this.open = msg.open;
1707
+ this.place();
1708
+ if (msg.open) void this.loadContext();
1709
+ break;
1710
+ case "widget.setMode":
1711
+ if (this.inline) break;
1712
+ this.geo.mode = msg.mode;
1713
+ this.writeGeometry(this.geo);
1714
+ this.place();
1715
+ break;
1716
+ case "widget.setDockSide":
1717
+ if (this.inline) break;
1718
+ this.geo.dockSide = msg.side;
1719
+ this.writeGeometry(this.geo);
1720
+ this.place();
1721
+ break;
1722
+ case "widget.snapCorner":
1723
+ if (this.inline) break;
1724
+ this.geo.corner = msg.corner;
1725
+ this.writeGeometry(this.geo);
1726
+ this.place();
1727
+ break;
1728
+ case "widget.setLauncherRegion": {
1729
+ if (this.inline || this.open) break;
1730
+ const vw = window.innerWidth;
1731
+ const vh = window.innerHeight;
1732
+ if (msg.w <= LAUNCHER && msg.h <= LAUNCHER) {
1733
+ this.place();
1734
+ } else {
1735
+ const region = this.geo.mode === "dock" ? dockChrome(this.geo, msg.w, msg.h, vw, vh) : floatChrome(this.geo, msg.w, msg.h, vw, vh);
1736
+ this.applyBox(region, msg.h > LAUNCHER ? "menu" : "pill");
1737
+ }
1738
+ break;
1739
+ }
1740
+ case "widget.resize":
1741
+ if (this.inline) break;
1742
+ if (this.hostEl && this.open && this.geo.mode === "float") {
1743
+ this.hostEl.style.height = `${Math.min(
1744
+ msg.height,
1745
+ Math.max(0, window.innerHeight - 2 * MARGIN)
1746
+ )}px`;
1747
+ }
1748
+ break;
1749
+ case "widget.resizeStart":
1750
+ if (this.inline) break;
1751
+ this.resizeBase = { w: this.geo.floatW, h: this.geo.floatH };
1752
+ this.dockBase = this.geo.dockW;
1753
+ if (this.hostEl) this.hostEl.style.transition = "none";
1754
+ break;
1755
+ case "widget.resizeMove": {
1756
+ if (this.inline || !this.resizeBase) break;
1757
+ if (this.geo.mode === "dock") {
1758
+ const w = applyDockResizeDelta(
1759
+ this.geo.dockSide,
1760
+ this.dockBase,
1761
+ msg.dx
1762
+ );
1763
+ this.geo.dockW = Math.min(
1764
+ Math.max(w, MIN_DOCK_W),
1765
+ Math.max(0, window.innerWidth - 2 * MARGIN)
1766
+ );
1767
+ } else {
1768
+ const next = applyResizeDelta(
1769
+ growthCorner(this.geo, window.innerWidth, window.innerHeight),
1770
+ this.resizeBase.w,
1771
+ this.resizeBase.h,
1772
+ msg.dx,
1773
+ msg.dy
1774
+ );
1775
+ const fit = clampSize(
1776
+ next.w,
1777
+ next.h,
1778
+ window.innerWidth,
1779
+ window.innerHeight,
1780
+ MIN_FLOAT_W,
1781
+ MIN_FLOAT_H
1782
+ );
1783
+ this.geo.floatW = fit.w;
1784
+ this.geo.floatH = fit.h;
1785
+ }
1786
+ this.place();
1787
+ break;
1788
+ }
1789
+ case "widget.resizeEnd":
1790
+ if (this.inline) break;
1791
+ this.resizeBase = null;
1792
+ if (this.hostEl) this.hostEl.style.transition = "";
1793
+ this.writeGeometry(this.geo);
1794
+ break;
1795
+ case "widget.requestContext":
1796
+ void this.loadContext().then((m) => m.provideContext());
1797
+ break;
1798
+ case "widget.requestSnapshot":
1799
+ void this.loadContext().then((m) => m.sendSnapshot(this.send));
1800
+ break;
1801
+ case "widget.readRegion":
1802
+ void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
1803
+ break;
1804
+ case "widget.callTool":
1805
+ void this.loadContext().then((m) => m.callTool(msg.call, this.send));
1806
+ break;
1807
+ // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
1808
+ // doesn't draw one), and moving or hiding the host's own panel from inside it
1809
+ // would be us redecorating their app. An older cached loader could still be told
1810
+ // any of these by a newer widget, so they're guarded rather than assumed absent.
1811
+ case "widget.dragStart":
1812
+ if (this.inline) break;
1813
+ this.dragFrom = dragAnchor(
1814
+ this.geo,
1815
+ window.innerWidth,
1816
+ window.innerHeight
1817
+ );
1818
+ this.dragAt = { ...this.dragFrom };
1819
+ this.dragLeftBand = false;
1820
+ this.dragGrowth = growthCorner(
1821
+ this.geo,
1822
+ window.innerWidth,
1823
+ window.innerHeight
1824
+ );
1825
+ this.dragBox = visibleBox(
1826
+ this.geo,
1827
+ this.open,
1828
+ window.innerWidth,
1829
+ window.innerHeight
1830
+ );
1831
+ if (this.hostEl) this.hostEl.style.transition = "none";
1832
+ break;
1833
+ case "widget.dragMove": {
1834
+ if (this.inline || !this.dragFrom) break;
1835
+ const vw = window.innerWidth;
1836
+ this.dragAt = {
1837
+ x: this.dragFrom.x + msg.dx,
1838
+ y: this.dragFrom.y + msg.dy
1839
+ };
1840
+ const side = edgeDock(this.dragBox.x + msg.dx, this.dragBox.w, vw);
1841
+ if (!side) this.dragLeftBand = true;
1842
+ if (side && (this.dragLeftBand || this.geo.mode === "dock")) {
1843
+ this.geo.mode = "dock";
1844
+ this.geo.dockSide = side;
1845
+ this.geo.tabY = Math.max(0, this.dragAt.y);
1846
+ } else {
1847
+ this.geo.mode = "float";
1848
+ this.geo.corner = null;
1849
+ this.geo.x = this.dragAt.x;
1850
+ this.geo.y = this.dragAt.y;
1851
+ }
1852
+ this.place();
1853
+ break;
1854
+ }
1855
+ case "widget.dragEnd": {
1856
+ if (this.inline || !this.dragFrom) break;
1857
+ const at = placeDrop(
1858
+ this.dragAt.x,
1859
+ this.dragAt.y,
1860
+ this.dragBox.x + (this.dragAt.x - this.dragFrom.x),
1861
+ this.dragBox.w,
1862
+ window.innerWidth,
1863
+ window.innerHeight
1864
+ );
1865
+ this.geo.mode = at.mode;
1866
+ if (at.mode === "dock") {
1867
+ this.geo.dockSide = at.dockSide;
1868
+ this.geo.tabY = at.tabY;
1869
+ } else {
1870
+ this.geo.corner = at.corner;
1871
+ this.geo.x = at.x;
1872
+ this.geo.y = at.y;
1873
+ }
1874
+ this.dragFrom = null;
1875
+ this.dragGrowth = null;
1876
+ if (this.hostEl) this.hostEl.style.transition = "";
1877
+ this.place();
1878
+ this.writeGeometry(this.geo);
1879
+ break;
1880
+ }
1881
+ case "widget.resetPos":
1882
+ if (this.inline) break;
1883
+ this.geo = defaultGeometry();
1884
+ this.writeGeometry(this.geo);
1885
+ this.place();
1886
+ break;
1887
+ case "widget.hide":
1888
+ if (this.inline) break;
1889
+ if (this.hostEl) this.hostEl.style.display = "none";
1890
+ break;
1891
+ case "widget.needsAuth":
1892
+ void this.provideAuth();
1893
+ break;
1894
+ }
1100
1895
  }
1101
1896
  /**
1102
1897
  * Own a drag for its lifetime.
@@ -1110,47 +1905,77 @@ var EmbedHost = class {
1110
1905
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
1111
1906
  * widget has to own the gesture. All we do is take a delta and place the element.
1112
1907
  */
1113
- cornerOffset() {
1114
- const rect = this.hostEl.getBoundingClientRect();
1115
- return {
1116
- right: window.innerWidth - rect.right,
1117
- bottom: window.innerHeight - rect.bottom
1118
- };
1908
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
1909
+ applyBox(box, mode) {
1910
+ if (!this.hostEl || this.inline) return;
1911
+ const s = this.hostEl.style;
1912
+ s.top = box.top;
1913
+ s.right = box.right;
1914
+ s.bottom = box.bottom;
1915
+ s.left = box.left;
1916
+ s.width = box.width;
1917
+ s.height = box.height;
1918
+ this.hostEl.dataset.mode = mode;
1919
+ if (mode === "tab" || mode === "dock")
1920
+ this.hostEl.dataset.flush = this.geo.dockSide;
1921
+ else delete this.hostEl.dataset.flush;
1922
+ }
1923
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
1924
+ /**
1925
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
1926
+ *
1927
+ * Every consumer has to agree on this, which is the bug that made the widget
1928
+ * flip-flop under small movements near the viewport centre: the box was frozen but
1929
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
1930
+ * swapping which corner it drew against (and it animates that), twitching back and
1931
+ * forth while the loader held perfectly still.
1932
+ */
1933
+ growthNow() {
1934
+ return this.dragGrowth ?? growthCorner(this.geo, window.innerWidth, window.innerHeight);
1119
1935
  }
1120
- dragTo(dx, dy) {
1121
- if (!this.dragBase || !this.hostEl) return;
1122
- const rect = this.hostEl.getBoundingClientRect();
1123
- const maxRight = Math.max(0, window.innerWidth - rect.width);
1124
- const maxBottom = Math.max(0, window.innerHeight - rect.height);
1125
- const right = Math.min(Math.max(0, this.dragBase.right - dx), maxRight);
1126
- const bottom = Math.min(Math.max(0, this.dragBase.bottom - dy), maxBottom);
1127
- this.pos = { right, bottom };
1128
- this.hostEl.style.right = `${right}px`;
1129
- this.hostEl.style.bottom = `${bottom}px`;
1130
- }
1131
- /** Reset to the default corner (a menu action — the drag is otherwise sticky). */
1132
- resetPos() {
1133
- this.pos = null;
1134
- this.writePos(null);
1135
- if (this.hostEl) {
1136
- this.hostEl.style.right = "20px";
1137
- this.hostEl.style.bottom = "20px";
1936
+ place() {
1937
+ const vw = window.innerWidth;
1938
+ const vh = window.innerHeight;
1939
+ if (!this.open) {
1940
+ const dock = this.geo.mode === "dock";
1941
+ this.applyBox(
1942
+ dock ? tabBox(this.geo, vw, vh) : floatChrome(this.geo, LAUNCHER, LAUNCHER, vw, vh, this.growthNow()),
1943
+ dock ? "tab" : "launcher"
1944
+ );
1945
+ } else if (this.geo.mode === "dock") {
1946
+ this.applyBox(dockBox(this.geo, vw), "dock");
1947
+ } else {
1948
+ this.applyBox(floatBox(this.geo, vw, vh, this.growthNow()), "float");
1138
1949
  }
1950
+ this.publishGeometry();
1951
+ }
1952
+ /**
1953
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
1954
+ * without this it draws its launcher against a stale guess — which is what made the
1955
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
1956
+ */
1957
+ publishGeometry() {
1958
+ this.send({
1959
+ type: "host.geometry",
1960
+ mode: this.geo.mode,
1961
+ dockSide: this.geo.dockSide,
1962
+ growth: this.growthNow()
1963
+ });
1964
+ }
1965
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
1966
+ themeMode() {
1967
+ return this.config.theme === "auto" ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : this.config.theme;
1139
1968
  }
1140
- readPos() {
1969
+ readGeometry() {
1141
1970
  try {
1142
- const raw = localStorage.getItem(POS_KEY);
1143
- if (!raw) return null;
1144
- const p = JSON.parse(raw);
1145
- return typeof p?.right === "number" && typeof p?.bottom === "number" ? { right: p.right, bottom: p.bottom } : null;
1971
+ return parseGeometry(localStorage.getItem(POS_KEY)) ?? defaultGeometry();
1146
1972
  } catch {
1147
- return null;
1973
+ return defaultGeometry();
1148
1974
  }
1149
1975
  }
1150
- writePos(pos) {
1976
+ writeGeometry(g) {
1151
1977
  try {
1152
- if (pos) localStorage.setItem(POS_KEY, JSON.stringify(pos));
1153
- else localStorage.removeItem(POS_KEY);
1978
+ localStorage.setItem(POS_KEY, serializeGeometry(g));
1154
1979
  } catch {
1155
1980
  }
1156
1981
  }
@@ -1174,6 +1999,7 @@ var EmbedHost = class {
1174
1999
  async provideAuth() {
1175
2000
  const provider = this.config.authTokenProvider ?? globalThis.matterfact?.getEmbedAuthToken;
1176
2001
  if (!provider) return;
2002
+ if (++this.ac > 5) return;
1177
2003
  try {
1178
2004
  const token = await provider();
1179
2005
  if (token) this.send({ type: "host.auth", token, expiresAt: 0 });
@@ -1205,6 +2031,7 @@ var EmbedHost = class {
1205
2031
  * calls this. */
1206
2032
  destroy() {
1207
2033
  window.removeEventListener("message", this.onMessage);
2034
+ window.removeEventListener("resize", this.onViewportResize);
1208
2035
  this.hostEl?.remove();
1209
2036
  this.hostEl = null;
1210
2037
  this.iframe = null;