@helpai/elements 0.30.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -3,11 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.ts
6
- import { h, render } from "preact";
7
-
8
- // src/ui/app.tsx
9
- import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
10
- import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
6
+ import { render } from "preact";
11
7
 
12
8
  // src/core/brand.ts
13
9
  var BRAND = {
@@ -935,20 +931,6 @@ var REACTIVE_ATTRS = [
935
931
  ...SPECIAL_REACTIVE_ATTRS
936
932
  ];
937
933
 
938
- // src/core/handshake-shape.ts
939
- function isPlainObject(raw) {
940
- return !!raw && typeof raw === "object" && !Array.isArray(raw);
941
- }
942
- function isSiteConfigShape(raw) {
943
- return isPlainObject(raw);
944
- }
945
- function isBlocksConfigShape(raw) {
946
- if (!isPlainObject(raw)) return false;
947
- if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
948
- if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
949
- return true;
950
- }
951
-
952
934
  // src/core/host-commands.ts
953
935
  function commandEventName(cmd) {
954
936
  return `${BRAND.cssPrefix}:${cmd}`;
@@ -969,6 +951,155 @@ function bindHostCommands(host, handlers) {
969
951
  };
970
952
  }
971
953
 
954
+ // src/core/ids.ts
955
+ function uuid7() {
956
+ const ts = Date.now();
957
+ const bytes = new Uint8Array(16);
958
+ bytes[0] = Math.floor(ts / 2 ** 40) & 255;
959
+ bytes[1] = Math.floor(ts / 2 ** 32) & 255;
960
+ bytes[2] = ts >>> 24 & 255;
961
+ bytes[3] = ts >>> 16 & 255;
962
+ bytes[4] = ts >>> 8 & 255;
963
+ bytes[5] = ts & 255;
964
+ fillRandom(bytes.subarray(6));
965
+ bytes[6] = bytes[6] & 15 | 112;
966
+ bytes[8] = bytes[8] & 63 | 128;
967
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
968
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
969
+ }
970
+ function fillRandom(view) {
971
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
972
+ crypto.getRandomValues(view);
973
+ return;
974
+ }
975
+ for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
976
+ }
977
+
978
+ // src/core/persistence.ts
979
+ var log2 = logger.scope("persistence");
980
+ function createPersistence(widgetId, storage = defaultStorage) {
981
+ const prefix = `${BRAND.cssPrefix}.${widgetId}`;
982
+ const KEY_VISITOR = `${prefix}.visitorId`;
983
+ const KEY_CONVERSATION = `${prefix}.conversationId`;
984
+ const KEY_USER_PREFS = `${prefix}.userPrefs`;
985
+ const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
986
+ const KEY_PANEL_SIZE = `${prefix}.panelSize`;
987
+ const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
988
+ const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
989
+ const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
990
+ const KEY_FORMS_DONE = `${prefix}.formsDone`;
991
+ const persistence = {
992
+ getVisitorId() {
993
+ const existing = storage.get(KEY_VISITOR);
994
+ if (existing) return existing;
995
+ const minted = uuid7();
996
+ storage.set(KEY_VISITOR, minted);
997
+ return minted;
998
+ },
999
+ setVisitorId(id) {
1000
+ log2.debug("setVisitorId (rebind)", { id, widgetId });
1001
+ storage.set(KEY_VISITOR, id);
1002
+ },
1003
+ loadConversationId() {
1004
+ return storage.get(KEY_CONVERSATION) ?? void 0;
1005
+ },
1006
+ saveConversationId(id) {
1007
+ log2.trace("saveConversationId", { id, widgetId });
1008
+ if (id) storage.set(KEY_CONVERSATION, id);
1009
+ else storage.remove(KEY_CONVERSATION);
1010
+ },
1011
+ loadUserPrefs() {
1012
+ const raw = storage.get(KEY_USER_PREFS);
1013
+ if (!raw) return {};
1014
+ try {
1015
+ const parsed = JSON.parse(raw);
1016
+ return isPlainObject(parsed) ? parsed : {};
1017
+ } catch (error) {
1018
+ log2.warn("loadUserPrefs parse failed", { error });
1019
+ return {};
1020
+ }
1021
+ },
1022
+ saveUserPrefs(s) {
1023
+ try {
1024
+ storage.set(KEY_USER_PREFS, JSON.stringify(s));
1025
+ } catch (error) {
1026
+ log2.warn("saveUserPrefs serialise failed", { error });
1027
+ }
1028
+ },
1029
+ patchUserPrefs(patch) {
1030
+ const current = persistence.loadUserPrefs();
1031
+ persistence.saveUserPrefs({ ...current, ...patch });
1032
+ },
1033
+ loadPanelOpen() {
1034
+ const raw = storage.get(KEY_PANEL_OPEN);
1035
+ if (raw === "1") return true;
1036
+ if (raw === "0") return false;
1037
+ return null;
1038
+ },
1039
+ savePanelOpen(open) {
1040
+ storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
1041
+ },
1042
+ loadPanelSize() {
1043
+ const raw = storage.get(KEY_PANEL_SIZE);
1044
+ return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
1045
+ },
1046
+ savePanelSize(size) {
1047
+ storage.set(KEY_PANEL_SIZE, size);
1048
+ },
1049
+ loadCalloutDismissed(currentText) {
1050
+ if (!currentText) return false;
1051
+ const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
1052
+ return !!dismissedText && dismissedText === currentText;
1053
+ },
1054
+ saveCalloutDismissed(currentText) {
1055
+ if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
1056
+ else storage.remove(KEY_CALLOUT_DISMISSED);
1057
+ },
1058
+ loadSidebarCollapsed() {
1059
+ const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
1060
+ if (raw === "1") return true;
1061
+ if (raw === "0") return false;
1062
+ return null;
1063
+ },
1064
+ saveSidebarCollapsed(collapsed) {
1065
+ storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
1066
+ },
1067
+ loadActiveModule() {
1068
+ return storage.get(KEY_ACTIVE_MODULE) || null;
1069
+ },
1070
+ saveActiveModule(id) {
1071
+ storage.set(KEY_ACTIVE_MODULE, id);
1072
+ },
1073
+ loadFormsDone() {
1074
+ const raw = storage.get(KEY_FORMS_DONE);
1075
+ if (!raw) return {};
1076
+ try {
1077
+ const parsed = JSON.parse(raw);
1078
+ return isPlainObject(parsed) ? parsed : {};
1079
+ } catch (error) {
1080
+ log2.warn("loadFormsDone parse failed", { error });
1081
+ return {};
1082
+ }
1083
+ },
1084
+ saveFormDone(id) {
1085
+ try {
1086
+ const done = persistence.loadFormsDone();
1087
+ done[id] = Date.now();
1088
+ storage.set(KEY_FORMS_DONE, JSON.stringify(done));
1089
+ } catch (error) {
1090
+ log2.warn("saveFormDone serialise failed", { error });
1091
+ }
1092
+ },
1093
+ clearConversation() {
1094
+ storage.remove(KEY_CONVERSATION);
1095
+ }
1096
+ };
1097
+ return persistence;
1098
+ }
1099
+ function isPlainObject(v) {
1100
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1101
+ }
1102
+
972
1103
  // src/styles/tokens.css
973
1104
  var tokens_default = ':host{--__P__-accent-user: __BRAND_ACCENT__;--__P__-accent: var(--__P__-accent-user);--__P__-accent-text-user: __BRAND_ACCENT_TEXT__;--__P__-accent-text: var(--__P__-accent-text-user);--__P__-radius: __BRAND_RADIUS__;--__P__-radius-sm: 8px;--__P__-radius-md: 10px;--__P__-radius-lg: 16px;--__P__-font: __BRAND_FONT__;--__P__-font-display: var(--__P__-font);--__P__-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;--__P__-bg: oklch(1 0 0);--__P__-bg-elevated: oklch(.9789 .0029 278.3);--__P__-surface: var(--__P__-bg-elevated);--__P__-muted: var(--__P__-fg-muted);--__P__-hover: oklch(.1686 .011 278.3 / .06);--__P__-fg: oklch(.1686 .011 278.3);--__P__-fg-muted: oklch(.4932 .0301 278.3);--__P__-border: oklch(.9312 .0084 278.3);--__P__-border-strong: oklch(.8757 .0131 278.3);--__P__-bubble-user: var(--__P__-accent);--__P__-bubble-user-text: var(--__P__-accent-text);--__P__-bubble-assistant: oklch(.9638 .0058 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-on-accent: var(--__P__-accent-text);--__P__-success: oklch(.6456 .1654 151.53);--__P__-warning: oklch(.7135 .1494 74.05);--__P__-danger: oklch(.5966 .1818 24.51);--__P__-danger-text: oklch(.5054 .1905 27.52);--__P__-danger-bg: oklch(.5966 .1818 24.51 / .1);--__P__-neutral: oklch(.7137 .0192 278.3);--__P__-focus: var(--__P__-accent);--__P__-shadow-panel: 0 8px 24px -8px rgba(13, 15, 20, .18), 0 24px 64px -24px rgba(13, 15, 20, .24);--__P__-shadow-card: 0 1px 2px rgba(13, 15, 20, .06), 0 4px 12px -6px rgba(13, 15, 20, .12);--__P__-backdrop: oklch(.1686 .011 278.3 / .55);--__P__-shadow-fab: 0 6px 16px -4px color-mix(in oklch, var(--__P__-accent) 45%, transparent), 0 12px 32px -12px rgba(13, 15, 20, .25);--__P__-z-fab: 2147483600;--__P__-z-panel: 2147483601;--__P__-panel-w: 380px;--__P__-panel-h: 600px;--__P__-fab-size: 56px;--__P__-space-1: 4px;--__P__-space-2: 8px;--__P__-space-3: 12px;--__P__-space-4: 16px;--__P__-space-5: 20px;--__P__-space-6: 24px;--__P__-space-8: 32px;--__P__-text-scale: 1;--__P__-text-xs: calc(12px * var(--__P__-text-scale));--__P__-text-sm: calc(13px * var(--__P__-text-scale));--__P__-text-base: calc(15px * var(--__P__-text-scale));--__P__-text-md: calc(16px * var(--__P__-text-scale));--__P__-text-lg: calc(18px * var(--__P__-text-scale));--__P__-text-xl: calc(22px * var(--__P__-text-scale));--__P__-text-2xl: calc(30px * var(--__P__-text-scale))}:host([data-text-size="small"]){--__P__-text-scale: .9}:host([data-text-size="large"]){--__P__-text-scale: 1.15}:host{--__P__-ease: cubic-bezier(.2, .7, .2, 1);--__P__-ease-out: cubic-bezier(.2, .7, .2, 1);--__P__-ease-in: cubic-bezier(.4, 0, 1, 1);--__P__-ease-in-out: cubic-bezier(.65, 0, .35, 1);--__P__-dur-quick: .12s;--__P__-dur-base: .22s;--__P__-dur-slow: .32s;font-family:var(--__P__-font);color:var(--__P__-fg);color-scheme:light;--__P__-color-scheme: light}:host([data-theme="dark"]){--__P__-bg: oklch(.1783 .0128 278.3);--__P__-bg-elevated: oklch(.2232 .0182 278.3);--__P__-fg: oklch(.9368 .01 278.3);--__P__-fg-muted: oklch(.7048 .0296 278.3);--__P__-border: oklch(.2752 .0266 278.3);--__P__-border-strong: oklch(.335 .0291 278.3);--__P__-hover: oklch(.9368 .01 278.3 / .08);--__P__-bubble-assistant: oklch(.2474 .0311 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-danger-text: oklch(.8077 .1035 19.57);--__P__-danger-bg: oklch(.5966 .1818 24.51 / .18);--__P__-shadow-panel: 0 8px 24px -8px rgba(0, 0, 0, .6), 0 24px 64px -24px rgba(0, 0, 0, .7);--__P__-shadow-card: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 12px -6px rgba(0, 0, 0, .5);--__P__-backdrop: rgba(0, 0, 0, .65);color-scheme:dark;--__P__-color-scheme: dark}@media(prefers-color-scheme:dark){:host([data-theme="auto"]){--__P__-bg: oklch(.1783 .0128 278.3);--__P__-bg-elevated: oklch(.2232 .0182 278.3);--__P__-fg: oklch(.9368 .01 278.3);--__P__-fg-muted: oklch(.7048 .0296 278.3);--__P__-border: oklch(.2752 .0266 278.3);--__P__-border-strong: oklch(.335 .0291 278.3);--__P__-hover: oklch(.9368 .01 278.3 / .08);--__P__-bubble-assistant: oklch(.2474 .0311 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-shadow-panel: 0 8px 24px -8px rgba(0, 0, 0, .6), 0 24px 64px -24px rgba(0, 0, 0, .7);color-scheme:dark;--__P__-color-scheme: dark}}@supports (color: oklch(from red l c h)){:host{--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.3, l, .62) c h);--__P__-accent-text: oklch(from var(--__P__-accent) clamp(0, (l / .623 - 1) * -infinity, 1) 0 h)}:host([data-theme="dark"]){--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.65, l, .85) c h)}@media(prefers-color-scheme:dark){:host([data-theme="auto"]){--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.65, l, .85) c h)}}}@media(prefers-reduced-motion:reduce){:host{--__P__-dur-quick: 1ms;--__P__-dur-base: 1ms;--__P__-dur-slow: 1ms}}\n';
974
1105
 
@@ -993,7 +1124,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
993
1124
  var CSS_TEXT = [tokens_default, reset_default, panel_default, standalone_default, page_default].join("\n").replace(/__P__/g, PREFIX).replaceAll("__BRAND_ACCENT_TEXT__", ACCENT_TEXT).replaceAll("__BRAND_ACCENT__", ACCENT).replaceAll("__BRAND_RADIUS__", RADIUS).replaceAll("__BRAND_FONT__", FONT);
994
1125
 
995
1126
  // src/shadow/mount.ts
996
- var log2 = logger.scope("mount");
1127
+ var log3 = logger.scope("mount");
997
1128
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
998
1129
  var HOST_CSS = {
999
1130
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1010,7 +1141,7 @@ function hostPositionForMode(mode) {
1010
1141
  }
1011
1142
  function createShadowMount(opts = {}) {
1012
1143
  const position = opts.position ?? "flow";
1013
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1144
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1014
1145
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1015
1146
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1016
1147
  customElements.define(tagName, class extends HTMLElement {
@@ -1094,6 +1225,123 @@ function applyHostAttributes(host, resolved) {
1094
1225
  applySize(host, resolved.size);
1095
1226
  }
1096
1227
 
1228
+ // src/shadow/iframe-mount.ts
1229
+ var log4 = logger.scope("iframe");
1230
+ var IFRAME_SRCDOC = '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;overflow:hidden}</style></head><body></body></html>';
1231
+ function createIframeMount(opts, position, initialMode = "closed", layoutMode = "floating", target = null) {
1232
+ log4.debug("createIframeMount", { position, initialMode, layoutMode, hasTarget: !!target });
1233
+ const iframe = document.createElement("iframe");
1234
+ iframe.title = `${BRAND.name} chat`;
1235
+ iframe.setAttribute("allow", "microphone; clipboard-write");
1236
+ sizeIframe(iframe, position, initialMode, layoutMode);
1237
+ const parent = layoutMode === "inline" && target ? target : document.body;
1238
+ parent.appendChild(iframe);
1239
+ const doc = iframe.contentDocument;
1240
+ if (!doc) throw new Error("iframe contentDocument unavailable (cross-origin?)");
1241
+ doc.open();
1242
+ doc.write(IFRAME_SRCDOC);
1243
+ doc.close();
1244
+ const mount2 = createShadowMount({ ...opts, target: doc.body });
1245
+ const mo = new MutationObserver(() => {
1246
+ sizeIframe(iframe, position, mount2.hostElement.dataset.mode ?? "closed", layoutMode);
1247
+ });
1248
+ mo.observe(mount2.hostElement, { attributes: true, attributeFilter: ["data-mode"] });
1249
+ return {
1250
+ ...mount2,
1251
+ iframeElement: iframe,
1252
+ destroy() {
1253
+ mo.disconnect();
1254
+ mount2.destroy();
1255
+ iframe.remove();
1256
+ }
1257
+ };
1258
+ }
1259
+ var BASE_CSS = "border:0;background:transparent;z-index:2147483647;pointer-events:auto;";
1260
+ var FLOATING_SIZE_BY_MODE = {
1261
+ closed: { w: "88px", h: "88px" },
1262
+ expanded: { w: "min(calc(100vw - 32px), 680px)", h: "min(calc(100vh - 32px), 860px)" },
1263
+ // `open` (normal) — also the fallback for any other non-fullscreen state.
1264
+ open: { w: "min(calc(100vw - 32px), 440px)", h: "min(calc(100vh - 32px), 700px)" }
1265
+ };
1266
+ function sizeIframe(iframe, position, mode, layoutMode) {
1267
+ if (mode === "fullscreen" || layoutMode === "standalone") {
1268
+ iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
1269
+ return;
1270
+ }
1271
+ if (layoutMode === "inline") {
1272
+ iframe.style.cssText = `${BASE_CSS}position:static;display:block;width:100%;height:100%;`;
1273
+ return;
1274
+ }
1275
+ const compact = typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches;
1276
+ if (compact && (mode === "open" || mode === "expanded")) {
1277
+ iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
1278
+ return;
1279
+ }
1280
+ const vKey = position.startsWith("top-") ? "top" : "bottom";
1281
+ const hKey = position.endsWith("-left") ? "left" : "right";
1282
+ const { w, h: h2 } = FLOATING_SIZE_BY_MODE[mode] ?? FLOATING_SIZE_BY_MODE.open;
1283
+ iframe.style.cssText = `${BASE_CSS}position:fixed;${vKey}:16px;${hKey}:16px;width:${w};height:${h2};`;
1284
+ }
1285
+
1286
+ // src/core/events.ts
1287
+ var EventBus = class {
1288
+ constructor() {
1289
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
1290
+ }
1291
+ /**
1292
+ * Register a listener for `name`.
1293
+ * @returns an `off` function that removes this listener.
1294
+ */
1295
+ on(name, fn) {
1296
+ let set = this.handlers.get(name);
1297
+ if (!set) {
1298
+ set = /* @__PURE__ */ new Set();
1299
+ this.handlers.set(name, set);
1300
+ }
1301
+ set.add(fn);
1302
+ return () => this.handlers.get(name)?.delete(fn);
1303
+ }
1304
+ /**
1305
+ * Dispatch an event synchronously to all listeners.
1306
+ * Listener errors are swallowed — the widget must not crash on user callbacks.
1307
+ */
1308
+ emit(name, payload) {
1309
+ const set = this.handlers.get(name);
1310
+ if (!set) return;
1311
+ for (const fn of set) {
1312
+ try {
1313
+ fn(payload);
1314
+ } catch {
1315
+ }
1316
+ }
1317
+ }
1318
+ /** Remove every listener. Called by the instance `destroy()`. */
1319
+ clear() {
1320
+ this.handlers.clear();
1321
+ }
1322
+ };
1323
+
1324
+ // src/core/boot.ts
1325
+ import { h, render as renderPreact } from "preact";
1326
+
1327
+ // src/ui/app.tsx
1328
+ import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
1329
+ import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
1330
+
1331
+ // src/core/handshake-shape.ts
1332
+ function isPlainObject2(raw) {
1333
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
1334
+ }
1335
+ function isSiteConfigShape(raw) {
1336
+ return isPlainObject2(raw);
1337
+ }
1338
+ function isBlocksConfigShape(raw) {
1339
+ if (!isPlainObject2(raw)) return false;
1340
+ if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
1341
+ if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
1342
+ return true;
1343
+ }
1344
+
1097
1345
  // src/ui/host-mode.ts
1098
1346
  function computeHostMode(configured, panelSize, isOpen) {
1099
1347
  if (configured === "page") return "page";
@@ -1144,30 +1392,6 @@ function createAuth(opts) {
1144
1392
  };
1145
1393
  }
1146
1394
 
1147
- // src/core/ids.ts
1148
- function uuid7() {
1149
- const ts = Date.now();
1150
- const bytes = new Uint8Array(16);
1151
- bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1152
- bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1153
- bytes[2] = ts >>> 24 & 255;
1154
- bytes[3] = ts >>> 16 & 255;
1155
- bytes[4] = ts >>> 8 & 255;
1156
- bytes[5] = ts & 255;
1157
- fillRandom(bytes.subarray(6));
1158
- bytes[6] = bytes[6] & 15 | 112;
1159
- bytes[8] = bytes[8] & 63 | 128;
1160
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1161
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1162
- }
1163
- function fillRandom(view) {
1164
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1165
- crypto.getRandomValues(view);
1166
- return;
1167
- }
1168
- for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1169
- }
1170
-
1171
1395
  // src/stream/types.ts
1172
1396
  var StreamError = class extends Error {
1173
1397
  constructor(message, code, status, cause) {
@@ -1180,7 +1404,7 @@ var StreamError = class extends Error {
1180
1404
  };
1181
1405
 
1182
1406
  // src/stream/parser.ts
1183
- var log3 = logger.scope("parser");
1407
+ var log5 = logger.scope("parser");
1184
1408
  async function* parseChatStream(response, signal7) {
1185
1409
  if (!response.ok) {
1186
1410
  const text = await response.text().catch(() => "");
@@ -1232,10 +1456,10 @@ ${part}` : part;
1232
1456
  if (!dataPayload || dataPayload === "[DONE]") return null;
1233
1457
  try {
1234
1458
  const chunk = JSON.parse(dataPayload);
1235
- log3.trace("chunk", { type: chunk.type, eventId });
1459
+ log5.trace("chunk", { type: chunk.type, eventId });
1236
1460
  return { chunk, eventId };
1237
1461
  } catch (err) {
1238
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1462
+ log5.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1239
1463
  return null;
1240
1464
  }
1241
1465
  }
@@ -1390,7 +1614,7 @@ function messageToWireParts(m) {
1390
1614
  }
1391
1615
 
1392
1616
  // src/stream/transport.ts
1393
- var log4 = logger.scope("transport");
1617
+ var log6 = logger.scope("transport");
1394
1618
  var MAX_RESUME_ATTEMPTS = 3;
1395
1619
  var RESUME_BACKOFF_MS = 400;
1396
1620
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1516,13 +1740,13 @@ var AgentTransport = class {
1516
1740
  }
1517
1741
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1518
1742
  async handshake(body) {
1519
- log4.debug("handshake \u2192", {
1743
+ log6.debug("handshake \u2192", {
1520
1744
  visitorId: body.visitorId,
1521
1745
  conversationId: body.conversationId,
1522
1746
  locale: body.locale
1523
1747
  });
1524
1748
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1525
- log4.debug("handshake \u2190", {
1749
+ log6.debug("handshake \u2190", {
1526
1750
  visitorId: res.visitorId,
1527
1751
  conversationId: res.conversationId,
1528
1752
  hasWelcome: !!res.welcome,
@@ -1530,7 +1754,7 @@ var AgentTransport = class {
1530
1754
  rebind: res.rebind
1531
1755
  });
1532
1756
  if (!isHandshakeResponseShape(res)) {
1533
- log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1757
+ log6.warn("handshake response did not match expected shape; using raw response", { response: res });
1534
1758
  }
1535
1759
  this.visitorId = res.visitorId ?? body.visitorId;
1536
1760
  this.conversationId = res.conversationId;
@@ -1538,11 +1762,11 @@ var AgentTransport = class {
1538
1762
  }
1539
1763
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1540
1764
  async uploadFile(file) {
1541
- log4.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1765
+ log6.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1542
1766
  const fd = new FormData();
1543
1767
  fd.append("file", file, file.name);
1544
1768
  const res = await this.postForm(this.uploadPath, fd, "upload");
1545
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1769
+ log6.debug("uploadFile \u2190", { id: res.id, url: res.url });
1546
1770
  return res;
1547
1771
  }
1548
1772
  /**
@@ -1550,12 +1774,12 @@ var AgentTransport = class {
1550
1774
  * Only used when `features.voice === 'server'`.
1551
1775
  */
1552
1776
  async transcribe(audio, mimeType = "audio/webm") {
1553
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1777
+ log6.debug("transcribe \u2192", { size: audio.size, mimeType });
1554
1778
  const fd = new FormData();
1555
1779
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1556
1780
  fd.append("mimeType", mimeType);
1557
1781
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1558
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1782
+ log6.debug("transcribe \u2190", { textLen: res.text.length });
1559
1783
  return res;
1560
1784
  }
1561
1785
  async listConversations(params = {}) {
@@ -1563,14 +1787,14 @@ var AgentTransport = class {
1563
1787
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1564
1788
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1565
1789
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1566
- log4.debug("listConversations \u2192", {
1790
+ log6.debug("listConversations \u2192", {
1567
1791
  visitorId: params.visitorId ?? this.visitorId,
1568
1792
  limit: params.limit,
1569
1793
  cursor: params.cursor
1570
1794
  });
1571
1795
  const res = await this.getJson(url.toString(), "listConversations");
1572
1796
  const conversations = res.conversations ?? [];
1573
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1797
+ log6.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1574
1798
  return {
1575
1799
  conversations: conversations.map((conversation) => ({
1576
1800
  conversationId: conversation.conversationId,
@@ -1586,10 +1810,10 @@ var AgentTransport = class {
1586
1810
  async loadConversation(conversationId) {
1587
1811
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1588
1812
  url.searchParams.set("conversationId", conversationId);
1589
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1813
+ log6.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1590
1814
  const res = await this.getJson(url.toString(), "loadConversation");
1591
1815
  const messages = res.messages ?? [];
1592
- log4.debug("loadConversation \u2190", {
1816
+ log6.debug("loadConversation \u2190", {
1593
1817
  conversationId: res.conversationId,
1594
1818
  canContinue: res.canContinue,
1595
1819
  messageCount: messages.length,
@@ -1611,11 +1835,11 @@ var AgentTransport = class {
1611
1835
  */
1612
1836
  async markRead(conversationId) {
1613
1837
  if (!this.visitorId || !conversationId) return;
1614
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1838
+ log6.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1615
1839
  try {
1616
1840
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1617
1841
  } catch (err) {
1618
- log4.debug("markRead failed (non-fatal)", { err });
1842
+ log6.debug("markRead failed (non-fatal)", { err });
1619
1843
  }
1620
1844
  }
1621
1845
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
@@ -1623,7 +1847,7 @@ var AgentTransport = class {
1623
1847
  const key = JSON.stringify(query, Object.keys(query).toSorted());
1624
1848
  const cached = this.contentCache.get(key);
1625
1849
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1626
- log4.debug("listContent \u2192 cache", query);
1850
+ log6.debug("listContent \u2192 cache", query);
1627
1851
  return cached.result;
1628
1852
  }
1629
1853
  const result = this.fetchContent(query).catch((err) => {
@@ -1639,10 +1863,10 @@ var AgentTransport = class {
1639
1863
  if (value === void 0) continue;
1640
1864
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1641
1865
  }
1642
- log4.debug("listContent \u2192", query);
1866
+ log6.debug("listContent \u2192", query);
1643
1867
  const res = await this.getJson(url.toString(), "listContent");
1644
1868
  const items = res.items ?? [];
1645
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1869
+ log6.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1646
1870
  return { ...res, items };
1647
1871
  }
1648
1872
  /**
@@ -1660,21 +1884,21 @@ var AgentTransport = class {
1660
1884
  async bootstrap(conversationId) {
1661
1885
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1662
1886
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1663
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1887
+ log6.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1664
1888
  const res = await this.getJson(url.toString(), "bootstrap");
1665
1889
  const forms = res.forms ?? [];
1666
1890
  const formSubmissions = res.formSubmissions ?? [];
1667
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1891
+ log6.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1668
1892
  return { forms, formSubmissions };
1669
1893
  }
1670
1894
  async saveUserPrefs(userPrefs) {
1671
- log4.debug("saveUserPrefs \u2192", {
1895
+ log6.debug("saveUserPrefs \u2192", {
1672
1896
  visitorId: this.visitorId,
1673
1897
  conversationId: this.conversationId,
1674
1898
  settingsKeys: Object.keys(userPrefs)
1675
1899
  });
1676
1900
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1677
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1901
+ log6.debug("saveUserPrefs \u2190", { ok: res.ok });
1678
1902
  return res;
1679
1903
  }
1680
1904
  get uploadPath() {
@@ -1695,15 +1919,15 @@ var AgentTransport = class {
1695
1919
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1696
1920
  */
1697
1921
  async submitForm(body) {
1698
- log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1922
+ log6.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1699
1923
  try {
1700
1924
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1701
1925
  } catch (err) {
1702
- log4.debug("submitForm failed (non-fatal)", { err });
1926
+ log6.debug("submitForm failed (non-fatal)", { err });
1703
1927
  }
1704
1928
  }
1705
1929
  sendMessage(body) {
1706
- log4.debug("message \u2192", {
1930
+ log6.debug("message \u2192", {
1707
1931
  conversationId: body.conversationId,
1708
1932
  visitorId: body.visitorId,
1709
1933
  messageCount: body.messages.length,
@@ -1783,17 +2007,17 @@ var AgentTransport = class {
1783
2007
  });
1784
2008
  } catch (err) {
1785
2009
  if (ctrl.signal.aborted) return;
1786
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
2010
+ log6.debug("resume fetch failed \u2014 retrying", { attempt, err });
1787
2011
  continue;
1788
2012
  }
1789
2013
  if (res.status === 204 || !res.ok) {
1790
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
2014
+ log6.debug("resume \u2192 no in-flight stream", { status: res.status });
1791
2015
  return;
1792
2016
  }
1793
2017
  const before = seenIds.size;
1794
2018
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1795
2019
  if (seenIds.size === before) {
1796
- log4.debug("resume \u2192 stream closed with no new events; nothing to resume");
2020
+ log6.debug("resume \u2192 stream closed with no new events; nothing to resume");
1797
2021
  return;
1798
2022
  }
1799
2023
  }
@@ -1820,14 +2044,14 @@ var AgentTransport = class {
1820
2044
  } catch (err) {
1821
2045
  if (ctrl.signal.aborted) return true;
1822
2046
  if (err instanceof StreamError && err.status !== void 0) throw err;
1823
- log4.debug("stream segment dropped", { err });
2047
+ log6.debug("stream segment dropped", { err });
1824
2048
  }
1825
2049
  return false;
1826
2050
  }
1827
2051
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1828
2052
  cancelStream(ctrl, conversationId) {
1829
2053
  if (ctrl.signal.aborted) return;
1830
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2054
+ log6.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1831
2055
  ctrl.abort();
1832
2056
  if (!this.visitorId || !conversationId) return;
1833
2057
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1836,7 +2060,7 @@ var AgentTransport = class {
1836
2060
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1837
2061
  body: JSON.stringify(this.withEnvelope({})),
1838
2062
  keepalive: true
1839
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2063
+ }).catch((err) => log6.debug("cancel POST failed (non-fatal)", { err }));
1840
2064
  }
1841
2065
  // ---- Low-level fetch helpers ------------------------------------------
1842
2066
  async *openMessageStream(body, signal7) {
@@ -1918,13 +2142,13 @@ var AgentTransport = class {
1918
2142
  if (attempt >= MAX_REQUEST_RETRIES) {
1919
2143
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1920
2144
  }
1921
- log4.debug("request network error \u2014 retrying", { label, attempt });
2145
+ log6.debug("request network error \u2014 retrying", { label, attempt });
1922
2146
  await sleep(backoffMs(attempt));
1923
2147
  continue;
1924
2148
  }
1925
2149
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1926
2150
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1927
- log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
2151
+ log6.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
1928
2152
  await sleep(wait);
1929
2153
  }
1930
2154
  }
@@ -2119,7 +2343,7 @@ var TRIGGER = {
2119
2343
  };
2120
2344
 
2121
2345
  // src/stream/reducer.ts
2122
- var log5 = logger.scope("reducer");
2346
+ var log7 = logger.scope("reducer");
2123
2347
  var StreamReducer = class {
2124
2348
  constructor(messagesSig) {
2125
2349
  __publicField(this, "messagesSig", messagesSig);
@@ -2141,13 +2365,13 @@ var StreamReducer = class {
2141
2365
  case "finish-step":
2142
2366
  return;
2143
2367
  case "finish":
2144
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2368
+ log7.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2145
2369
  m.status = "complete";
2146
2370
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2147
2371
  this.bumpDone(m);
2148
2372
  return;
2149
2373
  case "error":
2150
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2374
+ log7.warn("stream error chunk", { errorText: chunk.errorText });
2151
2375
  m.status = "error";
2152
2376
  m.errorText = chunk.errorText;
2153
2377
  this.bumpDone(m);
@@ -2177,235 +2401,110 @@ var StreamReducer = class {
2177
2401
  case "tool-input-start":
2178
2402
  case "tool-input-delta":
2179
2403
  case "tool-input-available":
2180
- case "tool-output-available":
2181
- case "tool-output-error":
2182
- case "tool-approval-request":
2183
- applyTool(m, chunk);
2184
- return;
2185
- case "file":
2186
- appendPart(m, { kind: "file", url: chunk.url, mediaType: chunk.mediaType });
2187
- return;
2188
- case "source-url": {
2189
- const parts = m.partsSig.value;
2190
- if (parts.some((p33) => p33.kind === "source" && p33.sourceId === chunk.sourceId)) return;
2191
- appendPart(m, { kind: "source", sourceId: chunk.sourceId, url: chunk.url, title: chunk.title });
2192
- return;
2193
- }
2194
- case "data-notification":
2195
- return;
2196
- default: {
2197
- const _exhaustive = chunk;
2198
- void _exhaustive;
2199
- }
2200
- }
2201
- }
2202
- /**
2203
- * Terminal-state bump (`finish`/`error`). Touches the message's own `partsSig`
2204
- * AND the list so subscribers re-render: a buffered bubble (subscribed to its
2205
- * parts) flips out of the loading indicator into the finished reply, and the
2206
- * list reflects the new `status`.
2207
- */
2208
- bumpDone(m) {
2209
- m.partsSig.value = [...m.partsSig.value];
2210
- this.messagesSig.value = [...this.messagesSig.value];
2211
- }
2212
- };
2213
- function ensureTextPart(m, kind, id) {
2214
- const existing = m.partsSig.value.find((p33) => p33.kind === kind && p33.id === id);
2215
- if (existing) return existing;
2216
- const part = { kind, id, textSig: signal2(""), doneSig: signal2(false) };
2217
- appendPart(m, part);
2218
- return part;
2219
- }
2220
- function ensureToolPart(m, toolCallId, toolName) {
2221
- const existing = m.partsSig.value.find((p33) => p33.kind === "tool" && p33.toolCallId === toolCallId);
2222
- if (existing) return existing;
2223
- const part = {
2224
- kind: "tool",
2225
- toolCallId,
2226
- toolName: toolName ?? "tool",
2227
- inputPartialSig: signal2(""),
2228
- inputSig: signal2(void 0),
2229
- outputSig: signal2(void 0),
2230
- errorSig: signal2(void 0),
2231
- stateSig: signal2("input"),
2232
- approvalSig: signal2(void 0)
2233
- };
2234
- appendPart(m, part);
2235
- return part;
2236
- }
2237
- function appendPart(m, part) {
2238
- m.partsSig.value = [...m.partsSig.value, part];
2239
- }
2240
- function applyTool(m, chunk) {
2241
- switch (chunk.type) {
2242
- case "tool-input-start":
2243
- ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2244
- return;
2245
- case "tool-input-delta": {
2246
- const p33 = ensureToolPart(m, chunk.toolCallId);
2247
- p33.inputPartialSig.value += chunk.delta;
2248
- return;
2249
- }
2250
- case "tool-input-available": {
2251
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2252
- p33.inputSig.value = chunk.input;
2253
- p33.stateSig.value = isAskUserInputTool(p33.toolName) ? "awaiting-input" : "awaiting";
2254
- return;
2255
- }
2256
- case "tool-approval-request": {
2257
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2258
- p33.approvalSig.value = { id: chunk.approvalId };
2259
- p33.stateSig.value = "awaiting-approval";
2260
- return;
2261
- }
2262
- case "tool-output-available": {
2263
- const p33 = ensureToolPart(m, chunk.toolCallId);
2264
- p33.outputSig.value = chunk.output;
2265
- p33.stateSig.value = "output";
2266
- return;
2267
- }
2268
- case "tool-output-error": {
2269
- const p33 = ensureToolPart(m, chunk.toolCallId);
2270
- p33.errorSig.value = chunk.errorText;
2271
- p33.stateSig.value = "error";
2272
- return;
2273
- }
2274
- default:
2275
- return;
2276
- }
2277
- }
2278
-
2279
- // src/core/persistence.ts
2280
- var log6 = logger.scope("persistence");
2281
- function createPersistence(widgetId, storage = defaultStorage) {
2282
- const prefix = `${BRAND.cssPrefix}.${widgetId}`;
2283
- const KEY_VISITOR = `${prefix}.visitorId`;
2284
- const KEY_CONVERSATION = `${prefix}.conversationId`;
2285
- const KEY_USER_PREFS = `${prefix}.userPrefs`;
2286
- const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
2287
- const KEY_PANEL_SIZE = `${prefix}.panelSize`;
2288
- const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
2289
- const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
2290
- const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
2291
- const KEY_FORMS_DONE = `${prefix}.formsDone`;
2292
- const persistence = {
2293
- getVisitorId() {
2294
- const existing = storage.get(KEY_VISITOR);
2295
- if (existing) return existing;
2296
- const minted = uuid7();
2297
- storage.set(KEY_VISITOR, minted);
2298
- return minted;
2299
- },
2300
- setVisitorId(id) {
2301
- log6.debug("setVisitorId (rebind)", { id, widgetId });
2302
- storage.set(KEY_VISITOR, id);
2303
- },
2304
- loadConversationId() {
2305
- return storage.get(KEY_CONVERSATION) ?? void 0;
2306
- },
2307
- saveConversationId(id) {
2308
- log6.trace("saveConversationId", { id, widgetId });
2309
- if (id) storage.set(KEY_CONVERSATION, id);
2310
- else storage.remove(KEY_CONVERSATION);
2311
- },
2312
- loadUserPrefs() {
2313
- const raw = storage.get(KEY_USER_PREFS);
2314
- if (!raw) return {};
2315
- try {
2316
- const parsed = JSON.parse(raw);
2317
- return isPlainObject2(parsed) ? parsed : {};
2318
- } catch (error) {
2319
- log6.warn("loadUserPrefs parse failed", { error });
2320
- return {};
2321
- }
2322
- },
2323
- saveUserPrefs(s) {
2324
- try {
2325
- storage.set(KEY_USER_PREFS, JSON.stringify(s));
2326
- } catch (error) {
2327
- log6.warn("saveUserPrefs serialise failed", { error });
2328
- }
2329
- },
2330
- patchUserPrefs(patch) {
2331
- const current = persistence.loadUserPrefs();
2332
- persistence.saveUserPrefs({ ...current, ...patch });
2333
- },
2334
- loadPanelOpen() {
2335
- const raw = storage.get(KEY_PANEL_OPEN);
2336
- if (raw === "1") return true;
2337
- if (raw === "0") return false;
2338
- return null;
2339
- },
2340
- savePanelOpen(open) {
2341
- storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
2342
- },
2343
- loadPanelSize() {
2344
- const raw = storage.get(KEY_PANEL_SIZE);
2345
- return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
2346
- },
2347
- savePanelSize(size) {
2348
- storage.set(KEY_PANEL_SIZE, size);
2349
- },
2350
- loadCalloutDismissed(currentText) {
2351
- if (!currentText) return false;
2352
- const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
2353
- return !!dismissedText && dismissedText === currentText;
2354
- },
2355
- saveCalloutDismissed(currentText) {
2356
- if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
2357
- else storage.remove(KEY_CALLOUT_DISMISSED);
2358
- },
2359
- loadSidebarCollapsed() {
2360
- const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
2361
- if (raw === "1") return true;
2362
- if (raw === "0") return false;
2363
- return null;
2364
- },
2365
- saveSidebarCollapsed(collapsed) {
2366
- storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
2367
- },
2368
- loadActiveModule() {
2369
- return storage.get(KEY_ACTIVE_MODULE) || null;
2370
- },
2371
- saveActiveModule(id) {
2372
- storage.set(KEY_ACTIVE_MODULE, id);
2373
- },
2374
- loadFormsDone() {
2375
- const raw = storage.get(KEY_FORMS_DONE);
2376
- if (!raw) return {};
2377
- try {
2378
- const parsed = JSON.parse(raw);
2379
- return isPlainObject2(parsed) ? parsed : {};
2380
- } catch (error) {
2381
- log6.warn("loadFormsDone parse failed", { error });
2382
- return {};
2404
+ case "tool-output-available":
2405
+ case "tool-output-error":
2406
+ case "tool-approval-request":
2407
+ applyTool(m, chunk);
2408
+ return;
2409
+ case "file":
2410
+ appendPart(m, { kind: "file", url: chunk.url, mediaType: chunk.mediaType });
2411
+ return;
2412
+ case "source-url": {
2413
+ const parts = m.partsSig.value;
2414
+ if (parts.some((p33) => p33.kind === "source" && p33.sourceId === chunk.sourceId)) return;
2415
+ appendPart(m, { kind: "source", sourceId: chunk.sourceId, url: chunk.url, title: chunk.title });
2416
+ return;
2383
2417
  }
2384
- },
2385
- saveFormDone(id) {
2386
- try {
2387
- const done = persistence.loadFormsDone();
2388
- done[id] = Date.now();
2389
- storage.set(KEY_FORMS_DONE, JSON.stringify(done));
2390
- } catch (error) {
2391
- log6.warn("saveFormDone serialise failed", { error });
2418
+ case "data-notification":
2419
+ return;
2420
+ default: {
2421
+ const _exhaustive = chunk;
2422
+ void _exhaustive;
2392
2423
  }
2393
- },
2394
- clearConversation() {
2395
- storage.remove(KEY_CONVERSATION);
2396
2424
  }
2425
+ }
2426
+ /**
2427
+ * Terminal-state bump (`finish`/`error`). Touches the message's own `partsSig`
2428
+ * AND the list so subscribers re-render: a buffered bubble (subscribed to its
2429
+ * parts) flips out of the loading indicator into the finished reply, and the
2430
+ * list reflects the new `status`.
2431
+ */
2432
+ bumpDone(m) {
2433
+ m.partsSig.value = [...m.partsSig.value];
2434
+ this.messagesSig.value = [...this.messagesSig.value];
2435
+ }
2436
+ };
2437
+ function ensureTextPart(m, kind, id) {
2438
+ const existing = m.partsSig.value.find((p33) => p33.kind === kind && p33.id === id);
2439
+ if (existing) return existing;
2440
+ const part = { kind, id, textSig: signal2(""), doneSig: signal2(false) };
2441
+ appendPart(m, part);
2442
+ return part;
2443
+ }
2444
+ function ensureToolPart(m, toolCallId, toolName) {
2445
+ const existing = m.partsSig.value.find((p33) => p33.kind === "tool" && p33.toolCallId === toolCallId);
2446
+ if (existing) return existing;
2447
+ const part = {
2448
+ kind: "tool",
2449
+ toolCallId,
2450
+ toolName: toolName ?? "tool",
2451
+ inputPartialSig: signal2(""),
2452
+ inputSig: signal2(void 0),
2453
+ outputSig: signal2(void 0),
2454
+ errorSig: signal2(void 0),
2455
+ stateSig: signal2("input"),
2456
+ approvalSig: signal2(void 0)
2397
2457
  };
2398
- return persistence;
2458
+ appendPart(m, part);
2459
+ return part;
2399
2460
  }
2400
- function isPlainObject2(v) {
2401
- return typeof v === "object" && v !== null && !Array.isArray(v);
2461
+ function appendPart(m, part) {
2462
+ m.partsSig.value = [...m.partsSig.value, part];
2463
+ }
2464
+ function applyTool(m, chunk) {
2465
+ switch (chunk.type) {
2466
+ case "tool-input-start":
2467
+ ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2468
+ return;
2469
+ case "tool-input-delta": {
2470
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2471
+ p33.inputPartialSig.value += chunk.delta;
2472
+ return;
2473
+ }
2474
+ case "tool-input-available": {
2475
+ const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2476
+ p33.inputSig.value = chunk.input;
2477
+ p33.stateSig.value = isAskUserInputTool(p33.toolName) ? "awaiting-input" : "awaiting";
2478
+ return;
2479
+ }
2480
+ case "tool-approval-request": {
2481
+ const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2482
+ p33.approvalSig.value = { id: chunk.approvalId };
2483
+ p33.stateSig.value = "awaiting-approval";
2484
+ return;
2485
+ }
2486
+ case "tool-output-available": {
2487
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2488
+ p33.outputSig.value = chunk.output;
2489
+ p33.stateSig.value = "output";
2490
+ return;
2491
+ }
2492
+ case "tool-output-error": {
2493
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2494
+ p33.errorSig.value = chunk.errorText;
2495
+ p33.stateSig.value = "error";
2496
+ return;
2497
+ }
2498
+ default:
2499
+ return;
2500
+ }
2402
2501
  }
2403
2502
 
2404
2503
  // src/core/feedback/index.ts
2405
2504
  import { signal as signal3 } from "@preact/signals";
2406
2505
 
2407
2506
  // src/core/feedback/audio.ts
2408
- var log7 = logger.scope("feedback:audio");
2507
+ var log8 = logger.scope("feedback:audio");
2409
2508
  var AudioEngine = class {
2410
2509
  constructor() {
2411
2510
  __publicField(this, "ctx", null);
@@ -2426,7 +2525,7 @@ var AudioEngine = class {
2426
2525
  this.master.gain.value = 1;
2427
2526
  this.master.connect(this.ctx.destination);
2428
2527
  } catch (err) {
2429
- log7.warn("AudioContext init failed", { err });
2528
+ log8.warn("AudioContext init failed", { err });
2430
2529
  }
2431
2530
  }
2432
2531
  /** Route all sounds through this multiplier. Call on volume change. */
@@ -2483,7 +2582,7 @@ var AudioEngine = class {
2483
2582
  this.buffers.set(url, buf);
2484
2583
  return buf;
2485
2584
  }).catch((err) => {
2486
- log7.warn("audio fetch/decode failed", { url, err });
2585
+ log8.warn("audio fetch/decode failed", { url, err });
2487
2586
  this.buffers.delete(url);
2488
2587
  return null;
2489
2588
  });
@@ -2569,7 +2668,7 @@ function acquireHaptics() {
2569
2668
  }
2570
2669
 
2571
2670
  // src/core/feedback/index.ts
2572
- var log8 = logger.scope("feedback");
2671
+ var log9 = logger.scope("feedback");
2573
2672
  var DEDUP_WINDOW_MS = 100;
2574
2673
  var FeedbackBus = class {
2575
2674
  constructor(sound, haptics) {
@@ -2647,7 +2746,7 @@ var FeedbackBus = class {
2647
2746
  const override = this.sound.events?.[event];
2648
2747
  if (override === false) return;
2649
2748
  if (typeof override === "string") {
2650
- this.audio.playUrl(override).catch((err) => log8.warn("audio play failed", { err, url: override }));
2749
+ this.audio.playUrl(override).catch((err) => log9.warn("audio play failed", { err, url: override }));
2651
2750
  return;
2652
2751
  }
2653
2752
  const tone = DEFAULT_TONES[event];
@@ -3292,7 +3391,7 @@ function AgentBadge({ agent }) {
3292
3391
  import { useEffect as useEffect4, useRef as useRef2, useState as useState2 } from "preact/hooks";
3293
3392
 
3294
3393
  // src/core/attachments.ts
3295
- var log9 = logger.scope("attachments");
3394
+ var log10 = logger.scope("attachments");
3296
3395
  function ingest(source, existing, limits) {
3297
3396
  const incoming = collectFiles(source);
3298
3397
  const accepted = [];
@@ -3311,7 +3410,7 @@ function ingest(source, existing, limits) {
3311
3410
  totalCount += 1;
3312
3411
  }
3313
3412
  if (accepted.length || rejected.length) {
3314
- log9.debug("ingest", {
3413
+ log10.debug("ingest", {
3315
3414
  accepted: accepted.length,
3316
3415
  rejected: rejected.map((r) => ({ name: r.file.name, reason: r.reason }))
3317
3416
  });
@@ -3322,7 +3421,7 @@ function revoke(att) {
3322
3421
  try {
3323
3422
  URL.revokeObjectURL(att.previewUrl);
3324
3423
  } catch (error) {
3325
- log9.debug("revokeObjectURL failed", { error });
3424
+ log10.debug("revokeObjectURL failed", { error });
3326
3425
  }
3327
3426
  }
3328
3427
  function reject(file, count, bytes, limits, accept) {
@@ -3498,7 +3597,7 @@ function pickSupportedMimeType() {
3498
3597
  // src/ui/composer.tsx
3499
3598
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
3500
3599
  var p7 = BRAND.cssPrefix;
3501
- var log10 = logger.scope("composer");
3600
+ var log11 = logger.scope("composer");
3502
3601
  function totalBytes(items) {
3503
3602
  return items.reduce((acc, a) => acc + a.size, 0);
3504
3603
  }
@@ -3551,7 +3650,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3551
3650
  attachFromDrop: (items) => {
3552
3651
  const result = ingest(items, attsRef.current, options.attachments);
3553
3652
  if (result.accepted.length === 0) return;
3554
- log10.info("attach (drop)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3653
+ log11.info("attach (drop)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3555
3654
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3556
3655
  setAtts((prev) => [...prev, ...result.accepted]);
3557
3656
  }
@@ -3572,7 +3671,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3572
3671
  const result = ingest(data.items, attsRef.current, options.attachments);
3573
3672
  if (result.accepted.length > 0) {
3574
3673
  e.preventDefault();
3575
- log10.info("attach (paste)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3674
+ log11.info("attach (paste)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3576
3675
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3577
3676
  setAtts((prev) => [...prev, ...result.accepted]);
3578
3677
  return;
@@ -3606,14 +3705,14 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3606
3705
  const input = e.target;
3607
3706
  const result = ingest(input.files, atts, options.attachments);
3608
3707
  if (result.accepted.length > 0) {
3609
- log10.info("attach (file picker)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3708
+ log11.info("attach (file picker)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3610
3709
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3611
3710
  setAtts((prev) => [...prev, ...result.accepted]);
3612
3711
  }
3613
3712
  input.value = "";
3614
3713
  };
3615
3714
  const removeAtt = (id) => {
3616
- log10.info("removeAttachment", { id });
3715
+ log11.info("removeAttachment", { id });
3617
3716
  bus.emit("removeAttachment", { id });
3618
3717
  setAtts((prev) => {
3619
3718
  const target = prev.find((a) => a.id === id);
@@ -3626,7 +3725,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3626
3725
  if (!voice.isSupported) return;
3627
3726
  if (voiceOn) {
3628
3727
  const durationMs = voiceStartedAtRef.current ? Date.now() - voiceStartedAtRef.current : 0;
3629
- log10.info("voiceStop", { durationMs });
3728
+ log11.info("voiceStop", { durationMs });
3630
3729
  bus.emit("voiceStop", { durationMs });
3631
3730
  voice.stop();
3632
3731
  setVoiceOn(false);
@@ -3635,7 +3734,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3635
3734
  return;
3636
3735
  }
3637
3736
  voiceStartedAtRef.current = Date.now();
3638
- log10.info("voiceStart");
3737
+ log11.info("voiceStart");
3639
3738
  bus.emit("voiceStart", void 0);
3640
3739
  setVoiceOn(true);
3641
3740
  feedback.play("voiceStart");
@@ -3645,7 +3744,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3645
3744
  setVoiceOn(false);
3646
3745
  if (voiceStartedAtRef.current) {
3647
3746
  const durationMs = Date.now() - voiceStartedAtRef.current;
3648
- log10.debug("voiceStop (auto)", { durationMs });
3747
+ log11.debug("voiceStop (auto)", { durationMs });
3649
3748
  bus.emit("voiceStop", { durationMs });
3650
3749
  voiceStartedAtRef.current = null;
3651
3750
  }
@@ -5206,7 +5305,7 @@ function startOfDay(ms) {
5206
5305
 
5207
5306
  // src/ui/conversation-list.tsx
5208
5307
  import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs15 } from "preact/jsx-runtime";
5209
- var log11 = logger.scope("history");
5308
+ var log12 = logger.scope("history");
5210
5309
  var BUCKET_TO_STRING = {
5211
5310
  today: "dateToday",
5212
5311
  yesterday: "dateYesterday",
@@ -5239,7 +5338,7 @@ function ConversationList({
5239
5338
  setState("loaded");
5240
5339
  }).catch((err) => {
5241
5340
  if (cancelled) return;
5242
- log11.warn("listConversations failed", { err });
5341
+ log12.warn("listConversations failed", { err });
5243
5342
  setState("error");
5244
5343
  });
5245
5344
  return () => {
@@ -5882,7 +5981,7 @@ function ModuleState({
5882
5981
  // src/ui/modules/help.tsx
5883
5982
  import { jsx as jsx27, jsxs as jsxs23 } from "preact/jsx-runtime";
5884
5983
  var p23 = BRAND.cssPrefix;
5885
- var log12 = logger.scope("help");
5984
+ var log13 = logger.scope("help");
5886
5985
  var openArticle = (nav, a) => a.url ? nav.push({ kind: "iframe", url: a.url, title: a.title }) : nav.push({ kind: "content", id: a.id, title: a.title });
5887
5986
  function groupByCategory(items) {
5888
5987
  const groups = /* @__PURE__ */ new Map();
@@ -5936,7 +6035,7 @@ function HelpRoot({ transport, strings, config, nav, panelProps }) {
5936
6035
  setState("loaded");
5937
6036
  }).catch((err) => {
5938
6037
  if (cancelled) return;
5939
- log12.warn("listContent (help) failed", { err });
6038
+ log13.warn("listContent (help) failed", { err });
5940
6039
  setErrorMsg(errorMessageFor(err, strings));
5941
6040
  setState("error");
5942
6041
  });
@@ -5987,7 +6086,7 @@ function HomeCard({ onClick, children, testid }) {
5987
6086
  // src/ui/modules/home.tsx
5988
6087
  import { Fragment as Fragment6, jsx as jsx29, jsxs as jsxs24 } from "preact/jsx-runtime";
5989
6088
  var p25 = BRAND.cssPrefix;
5990
- var log13 = logger.scope("home");
6089
+ var log14 = logger.scope("home");
5991
6090
  function resolveGreeting(props2) {
5992
6091
  const name = props2.options.userContext?.name;
5993
6092
  const fromConfig = props2.config.greetingText;
@@ -6006,7 +6105,7 @@ function HomeRoot(props2) {
6006
6105
  useEffect12(() => {
6007
6106
  if (!config.showRecentConversations) return;
6008
6107
  let cancelled = false;
6009
- transport.listConversations({ limit: 1 }).then((res) => !cancelled && setRecent(res.conversations?.[0] ?? null)).catch((err) => !cancelled && log13.warn("listConversations (home) failed", { err }));
6108
+ transport.listConversations({ limit: 1 }).then((res) => !cancelled && setRecent(res.conversations?.[0] ?? null)).catch((err) => !cancelled && log14.warn("listConversations (home) failed", { err }));
6010
6109
  return () => {
6011
6110
  cancelled = true;
6012
6111
  };
@@ -6014,7 +6113,7 @@ function HomeRoot(props2) {
6014
6113
  useEffect12(() => {
6015
6114
  if (!config.contentTags?.length) return;
6016
6115
  let cancelled = false;
6017
- transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log13.warn("listContent (home) failed", { err }));
6116
+ transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log14.warn("listContent (home) failed", { err }));
6018
6117
  return () => {
6019
6118
  cancelled = true;
6020
6119
  };
@@ -6088,7 +6187,7 @@ var homeLayout = {
6088
6187
  import { useEffect as useEffect13, useState as useState10 } from "preact/hooks";
6089
6188
  import { jsx as jsx30, jsxs as jsxs25 } from "preact/jsx-runtime";
6090
6189
  var p26 = BRAND.cssPrefix;
6091
- var log14 = logger.scope("news");
6190
+ var log15 = logger.scope("news");
6092
6191
  function NewsRoot({ transport, strings, config, nav, panelProps }) {
6093
6192
  const tags = config.contentTags;
6094
6193
  const [state, setState] = useState10("loading");
@@ -6104,7 +6203,7 @@ function NewsRoot({ transport, strings, config, nav, panelProps }) {
6104
6203
  setState("loaded");
6105
6204
  }).catch((err) => {
6106
6205
  if (cancelled) return;
6107
- log14.warn("listContent (news) failed", { err });
6206
+ log15.warn("listContent (news) failed", { err });
6108
6207
  setErrorMsg(errorMessageFor(err, strings));
6109
6208
  setState("error");
6110
6209
  });
@@ -6219,7 +6318,7 @@ function IframeView({ url, title, strings, onBack, actions }) {
6219
6318
  import { useCallback as useCallback3, useEffect as useEffect14, useState as useState11 } from "preact/hooks";
6220
6319
  import { jsx as jsx33, jsxs as jsxs28 } from "preact/jsx-runtime";
6221
6320
  var p29 = BRAND.cssPrefix;
6222
- var log15 = logger.scope("content");
6321
+ var log16 = logger.scope("content");
6223
6322
  function ContentView({ id, title, transport, strings, onBack, actions }) {
6224
6323
  const [item, setItem] = useState11(null);
6225
6324
  const [failed, setFailed] = useState11(false);
@@ -6238,7 +6337,7 @@ function ContentView({ id, title, transport, strings, onBack, actions }) {
6238
6337
  else setFailed(true);
6239
6338
  }).catch((err) => {
6240
6339
  if (cancelled) return;
6241
- log15.warn("listContent (reader) failed", { err });
6340
+ log16.warn("listContent (reader) failed", { err });
6242
6341
  setFailed(true);
6243
6342
  });
6244
6343
  return () => {
@@ -6458,7 +6557,7 @@ function useLauncherCallout({ callout, persistence }) {
6458
6557
 
6459
6558
  // src/ui/app.tsx
6460
6559
  import { jsx as jsx36, jsxs as jsxs31 } from "preact/jsx-runtime";
6461
- var log16 = logger.scope("app");
6560
+ var log17 = logger.scope("app");
6462
6561
  var p32 = BRAND.cssPrefix;
6463
6562
  function App({ options, hostElement, bus }) {
6464
6563
  const [persistence] = useState13(() => createPersistence(options.widgetId, options.storage));
@@ -6516,7 +6615,7 @@ function App({ options, hostElement, bus }) {
6516
6615
  });
6517
6616
  const calloutPersistent = options.launcher.callout?.persistent ?? true;
6518
6617
  const dismissCallout = useCallback6(() => {
6519
- log16.info("calloutDismiss");
6618
+ log17.info("calloutDismiss");
6520
6619
  bus.emit("calloutDismiss", void 0);
6521
6620
  dismissCalloutRaw();
6522
6621
  }, [bus, dismissCalloutRaw]);
@@ -6550,7 +6649,7 @@ function App({ options, hostElement, bus }) {
6550
6649
  (patch) => {
6551
6650
  persistence.patchUserPrefs(patch);
6552
6651
  transport.saveUserPrefs(persistence.loadUserPrefs()).catch((err) => {
6553
- log16.warn("saveUserPrefs failed", { err });
6652
+ log17.warn("saveUserPrefs failed", { err });
6554
6653
  });
6555
6654
  },
6556
6655
  [persistence, transport]
@@ -6631,7 +6730,7 @@ function App({ options, hostElement, bus }) {
6631
6730
  values: rec.values
6632
6731
  }));
6633
6732
  } catch (err) {
6634
- log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6733
+ log17.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6635
6734
  return [];
6636
6735
  }
6637
6736
  },
@@ -6671,7 +6770,7 @@ function App({ options, hostElement, bus }) {
6671
6770
  }
6672
6771
  } catch (err) {
6673
6772
  if (isStale()) return;
6674
- log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
6773
+ log17.warn("loadThread failed; resetting conversationId", { err, conversationId });
6675
6774
  conversationIdSig.value = void 0;
6676
6775
  persistence.saveConversationId(void 0);
6677
6776
  } finally {
@@ -6710,7 +6809,7 @@ function App({ options, hostElement, bus }) {
6710
6809
  }
6711
6810
  if (isStale()) return;
6712
6811
  if (res.visitorId && res.visitorId !== visitorId) {
6713
- log16.info("visitor rebound", { previous: visitorId, current: res.visitorId, reason: res.rebind?.visitorId });
6812
+ log17.info("visitor rebound", { previous: visitorId, current: res.visitorId, reason: res.rebind?.visitorId });
6714
6813
  persistence.setVisitorId(res.visitorId);
6715
6814
  setVisitorId(res.visitorId);
6716
6815
  bus.emit("visitorRebound", {
@@ -6720,7 +6819,7 @@ function App({ options, hostElement, bus }) {
6720
6819
  });
6721
6820
  }
6722
6821
  if (res.conversationId && res.conversationId !== conversationId) {
6723
- log16.info("conversation rebound", {
6822
+ log17.info("conversation rebound", {
6724
6823
  previous: conversationId,
6725
6824
  current: res.conversationId,
6726
6825
  reason: res.rebind?.conversationId
@@ -6735,9 +6834,9 @@ function App({ options, hostElement, bus }) {
6735
6834
  if (res.welcome?.suggestions) setSuggestions(res.welcome.suggestions);
6736
6835
  if (options.mode === "page") {
6737
6836
  if (res.site && isSiteConfigShape(res.site)) setParsedSite(res.site);
6738
- else if (res.site) log16.warn("invalid site config, using fallback", { site: res.site });
6837
+ else if (res.site) log17.warn("invalid site config, using fallback", { site: res.site });
6739
6838
  if (res.blocks && isBlocksConfigShape(res.blocks)) setParsedBlocks(res.blocks);
6740
- else if (res.blocks) log16.warn("invalid blocks config, using fallback", { blocks: res.blocks });
6839
+ else if (res.blocks) log17.warn("invalid blocks config, using fallback", { blocks: res.blocks });
6741
6840
  }
6742
6841
  if (res.userPrefs && !newConversation) {
6743
6842
  persistence.saveUserPrefs(res.userPrefs);
@@ -6803,7 +6902,7 @@ function App({ options, hostElement, bus }) {
6803
6902
  resumeActiveRef.current = true;
6804
6903
  const chatTab = chatTabIdRef.current;
6805
6904
  if (chatTab) homeNav.switchTab(chatTab);
6806
- log16.info("resumed in-flight reply on mount");
6905
+ log17.info("resumed in-flight reply on mount");
6807
6906
  }
6808
6907
  reducer.apply(evt.chunk);
6809
6908
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) setCanSend(false);
@@ -6819,7 +6918,7 @@ function App({ options, hostElement, bus }) {
6819
6918
  bubble.errorText = errorMessageFor(err, options.strings);
6820
6919
  feedback.play("error");
6821
6920
  }
6822
- log16.debug("mount resume ended without completing", { err });
6921
+ log17.debug("mount resume ended without completing", { err });
6823
6922
  } finally {
6824
6923
  resumeBubbleRef.current = null;
6825
6924
  if (bubble) {
@@ -6850,7 +6949,7 @@ function App({ options, hostElement, bus }) {
6850
6949
  const res = await dataBootRef.current;
6851
6950
  setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
6852
6951
  } catch (err) {
6853
- log16.debug("bootstrap failed \u2014 treating as no forms", { err });
6952
+ log17.debug("bootstrap failed \u2014 treating as no forms", { err });
6854
6953
  } finally {
6855
6954
  setFormsReady(true);
6856
6955
  }
@@ -6950,7 +7049,7 @@ function App({ options, hostElement, bus }) {
6950
7049
  (pt) => pt.kind === "tool" && pt.toolCallId === toolCallId
6951
7050
  );
6952
7051
  if (!part) {
6953
- log16.warn("resolveTool: tool part not found", { toolCallId });
7052
+ log17.warn("resolveTool: tool part not found", { toolCallId });
6954
7053
  return;
6955
7054
  }
6956
7055
  mutate(part);
@@ -6964,7 +7063,7 @@ function App({ options, hostElement, bus }) {
6964
7063
  );
6965
7064
  const handleToolResult = useCallback6(
6966
7065
  (toolCallId, output) => {
6967
- log16.info("toolResult", { toolCallId });
7066
+ log17.info("toolResult", { toolCallId });
6968
7067
  bus.emit("toolResult", { toolCallId });
6969
7068
  resolveTool(toolCallId, TRIGGER.toolResult, (part) => {
6970
7069
  part.outputSig.value = output;
@@ -6975,7 +7074,7 @@ function App({ options, hostElement, bus }) {
6975
7074
  );
6976
7075
  const handleToolDecision = useCallback6(
6977
7076
  (toolCallId, approved, reason) => {
6978
- log16.info("toolDecision", { toolCallId, approved });
7077
+ log17.info("toolDecision", { toolCallId, approved });
6979
7078
  bus.emit("toolDecision", { toolCallId, approved });
6980
7079
  resolveTool(toolCallId, TRIGGER.toolApproval, (part) => {
6981
7080
  const id = part.approvalSig.value?.id ?? toolCallId;
@@ -6999,7 +7098,7 @@ function App({ options, hostElement, bus }) {
6999
7098
  pageArea: () => options.pageContext?.area ? String(options.pageContext.area) : void 0,
7000
7099
  conversationId: () => conversationIdSig.value,
7001
7100
  onComplete: (form, trigger, values, skipped) => {
7002
- log16.info("formSubmit", { formId: form.id, trigger, skipped });
7101
+ log17.info("formSubmit", { formId: form.id, trigger, skipped });
7003
7102
  bus.emit("formSubmit", { formId: form.id, values, skipped });
7004
7103
  setFormMarkers((prev) => [
7005
7104
  ...prev.filter((m) => !(m.formId === form.id && m.outcome === "skipped")),
@@ -7044,12 +7143,12 @@ function App({ options, hostElement, bus }) {
7044
7143
  }, [idleMs, isOpen, forms, msgCount.value]);
7045
7144
  const handleSend = useCallback6(
7046
7145
  async (text, attachments) => {
7047
- log16.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7146
+ log17.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7048
7147
  if (streaming) return;
7049
7148
  bus.emit("send", { text, attachmentCount: attachments.length });
7050
7149
  if (suggestions.length > 0) setSuggestions([]);
7051
7150
  const uploaded = await uploadAttachments(transport, attachments, (err) => {
7052
- log16.warn("upload failed", { err });
7151
+ log17.warn("upload failed", { err });
7053
7152
  bus.emit("error", err);
7054
7153
  options.onError?.(err);
7055
7154
  });
@@ -7066,24 +7165,24 @@ function App({ options, hostElement, bus }) {
7066
7165
  [streaming, transport, bus, options, suggestions.length, messagesSig, reducer, streamAssistant, forms]
7067
7166
  );
7068
7167
  const handleStop = useCallback6(() => {
7069
- log16.info("stop");
7168
+ log17.info("stop");
7070
7169
  bus.emit("stop", void 0);
7071
7170
  activeCancel?.();
7072
7171
  }, [activeCancel, bus]);
7073
7172
  const handleSoundToggle = useCallback6(() => {
7074
7173
  feedback.toggleMuted();
7075
7174
  const muted = feedback.mutedSig.value;
7076
- log16.info("soundToggle", { muted });
7175
+ log17.info("soundToggle", { muted });
7077
7176
  bus.emit("soundToggle", { muted });
7078
7177
  }, [feedback, bus]);
7079
7178
  const handleClose = useCallback6(() => {
7080
7179
  if (resolveCloseAction(options.mode, panelSize) === "exit-fullscreen") {
7081
- log16.info("close \u2192 exit-fullscreen", { mode: options.mode });
7180
+ log17.info("close \u2192 exit-fullscreen", { mode: options.mode });
7082
7181
  setPanelSize("normal");
7083
7182
  bus.emit("fullscreen", false);
7084
7183
  return;
7085
7184
  }
7086
- log16.info("close \u2192 panel closed", { mode: options.mode });
7185
+ log17.info("close \u2192 panel closed", { mode: options.mode });
7087
7186
  forms.fire("panel-close");
7088
7187
  setIsOpen(false);
7089
7188
  persistence.savePanelOpen(false);
@@ -7094,7 +7193,7 @@ function App({ options, hostElement, bus }) {
7094
7193
  if (!options.modules.list.some((m) => m.layout === "chat")) return;
7095
7194
  transport.listConversations({ limit: 50 }).then((res) => {
7096
7195
  unreadCountSig.value = res.conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0);
7097
- }).catch((err) => log16.debug("refreshUnread failed (non-fatal)", { err }));
7196
+ }).catch((err) => log17.debug("refreshUnread failed (non-fatal)", { err }));
7098
7197
  }, [transport, options.modules, unreadCountSig]);
7099
7198
  const unreadSeeded = useRef9(false);
7100
7199
  useEffect17(() => {
@@ -7103,7 +7202,7 @@ function App({ options, hostElement, bus }) {
7103
7202
  refreshUnread();
7104
7203
  }, [activated, conversationReady, refreshUnread]);
7105
7204
  const handleOpen = useCallback6(() => {
7106
- log16.info("open", { mode: options.mode });
7205
+ log17.info("open", { mode: options.mode });
7107
7206
  feedback.unlock();
7108
7207
  setLauncherLeaving(true);
7109
7208
  setIsOpen(true);
@@ -7117,11 +7216,11 @@ function App({ options, hostElement, bus }) {
7117
7216
  const handleClear = useCallback6(() => {
7118
7217
  const hasUserMessages = messagesSig.value.some((m) => m.role === "user");
7119
7218
  if (!hasUserMessages && canSend && conversationIdSig.value) {
7120
- log16.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7219
+ log17.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7121
7220
  if (messagesSig.value.length === 0) playWelcome(welcomeRef.current?.messages);
7122
7221
  return;
7123
7222
  }
7124
- log16.info("clear \u2192 new conversation", { wasStreaming: streaming });
7223
+ log17.info("clear \u2192 new conversation", { wasStreaming: streaming });
7125
7224
  bus.emit("clear", void 0);
7126
7225
  if (streaming) activeCancel?.();
7127
7226
  messagesSig.value = [];
@@ -7139,14 +7238,14 @@ function App({ options, hostElement, bus }) {
7139
7238
  }, [handleClear, homeNav, options]);
7140
7239
  const handleExpand = useCallback6(() => {
7141
7240
  const nextSize = panelSize === "expanded" ? "normal" : "expanded";
7142
- log16.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7241
+ log17.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7143
7242
  setPanelSize(nextSize);
7144
7243
  persistence.savePanelSize(nextSize);
7145
7244
  bus.emit("expand", nextSize === "expanded");
7146
7245
  }, [bus, panelSize, persistence]);
7147
7246
  const handleFullscreen = useCallback6(() => {
7148
7247
  const nextSize = panelSize === "fullscreen" ? "normal" : "fullscreen";
7149
- log16.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7248
+ log17.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7150
7249
  setPanelSize(nextSize);
7151
7250
  persistence.savePanelSize(nextSize);
7152
7251
  bus.emit("fullscreen", nextSize === "fullscreen");
@@ -7156,21 +7255,21 @@ function App({ options, hostElement, bus }) {
7156
7255
  const url = new URL(options.popOutUrl);
7157
7256
  if (conversationIdSig.value) url.searchParams.set("conversation", conversationIdSig.value);
7158
7257
  if (options.widgetId !== "default") url.searchParams.set("widgetId", options.widgetId);
7159
- log16.info("popOut", { url: url.toString() });
7258
+ log17.info("popOut", { url: url.toString() });
7160
7259
  window.open(url.toString(), "_blank", "noopener,noreferrer");
7161
7260
  bus.emit("popOut", void 0);
7162
7261
  }, [bus, conversationIdSig, options.popOutUrl, options.widgetId]);
7163
7262
  const handleToggleHistory = useCallback6(() => {
7164
7263
  setView((v) => {
7165
7264
  const next = v === "history" ? "chat" : "history";
7166
- log16.info("toggleHistory", { view: next });
7265
+ log17.info("toggleHistory", { view: next });
7167
7266
  bus.emit("toggleHistory", { view: next });
7168
7267
  return next;
7169
7268
  });
7170
7269
  }, [bus]);
7171
7270
  const handleLocaleChange = useCallback6(
7172
7271
  (locale) => {
7173
- log16.info("localeChange", { locale });
7272
+ log17.info("localeChange", { locale });
7174
7273
  setActiveLocale(locale);
7175
7274
  patchAndSync({ locale });
7176
7275
  bus.emit("localeChange", locale);
@@ -7179,7 +7278,7 @@ function App({ options, hostElement, bus }) {
7179
7278
  );
7180
7279
  const handleThemeChange = useCallback6(
7181
7280
  (mode) => {
7182
- log16.info("themeChange", { mode });
7281
+ log17.info("themeChange", { mode });
7183
7282
  setActiveThemeMode(mode);
7184
7283
  hostElement.dataset.theme = mode;
7185
7284
  patchAndSync({ themeMode: mode });
@@ -7189,7 +7288,7 @@ function App({ options, hostElement, bus }) {
7189
7288
  );
7190
7289
  const handleTextSizeChange = useCallback6(
7191
7290
  (size) => {
7192
- log16.info("textSizeChange", { size });
7291
+ log17.info("textSizeChange", { size });
7193
7292
  setActiveTextSize(size);
7194
7293
  hostElement.dataset.textSize = size;
7195
7294
  patchAndSync({ textSize: size });
@@ -7200,7 +7299,7 @@ function App({ options, hostElement, bus }) {
7200
7299
  const handleToggleSidebarCollapsed = useCallback6(() => {
7201
7300
  setSidebarCollapsed((prev) => {
7202
7301
  const next = !prev;
7203
- log16.info("sidebarToggle", { collapsed: next });
7302
+ log17.info("sidebarToggle", { collapsed: next });
7204
7303
  persistence.saveSidebarCollapsed(next);
7205
7304
  bus.emit("sidebarToggle", { collapsed: next });
7206
7305
  return next;
@@ -7208,7 +7307,7 @@ function App({ options, hostElement, bus }) {
7208
7307
  }, [persistence, bus]);
7209
7308
  const handleWidgetSizeChange = useCallback6(
7210
7309
  (size) => {
7211
- log16.info("resize", size);
7310
+ log17.info("resize", size);
7212
7311
  patchAndSync({ widgetSize: size });
7213
7312
  bus.emit("resize", size);
7214
7313
  },
@@ -7216,7 +7315,7 @@ function App({ options, hostElement, bus }) {
7216
7315
  );
7217
7316
  const handleSelectHistoryConversation = useCallback6(
7218
7317
  async (targetConversationId) => {
7219
- log16.info("selectConversation", { conversationId: targetConversationId });
7318
+ log17.info("selectConversation", { conversationId: targetConversationId });
7220
7319
  bus.emit("selectConversation", { conversationId: targetConversationId });
7221
7320
  try {
7222
7321
  const [res, markers] = await Promise.all([
@@ -7318,7 +7417,7 @@ function App({ options, hostElement, bus }) {
7318
7417
  onStop: handleStop,
7319
7418
  onSuggestion: (s) => {
7320
7419
  const text = s.text ?? s.label;
7321
- log16.info("suggestion", { text });
7420
+ log17.info("suggestion", { text });
7322
7421
  bus.emit("suggestion", { text });
7323
7422
  void handleSend(text, []);
7324
7423
  },
@@ -7440,14 +7539,14 @@ function emitMessage(bus, options, role, text) {
7440
7539
 
7441
7540
  // src/ui/error-boundary.tsx
7442
7541
  import { Component } from "preact";
7443
- var log17 = logger.scope("error-boundary");
7542
+ var log18 = logger.scope("error-boundary");
7444
7543
  var ErrorBoundary = class extends Component {
7445
7544
  constructor() {
7446
7545
  super(...arguments);
7447
7546
  __publicField(this, "state", { crashed: false });
7448
7547
  }
7449
7548
  componentDidCatch(error, info) {
7450
- log17.error("widget render crashed", { error, componentStack: info?.componentStack });
7549
+ log18.error("widget render crashed", { error, componentStack: info?.componentStack });
7451
7550
  this.setState({ crashed: true });
7452
7551
  this.props.onError(error, info?.componentStack);
7453
7552
  }
@@ -7456,102 +7555,6 @@ var ErrorBoundary = class extends Component {
7456
7555
  }
7457
7556
  };
7458
7557
 
7459
- // src/shadow/iframe-mount.ts
7460
- var log18 = logger.scope("iframe");
7461
- var IFRAME_SRCDOC = '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;overflow:hidden}</style></head><body></body></html>';
7462
- function createIframeMount(opts, position, initialMode = "closed", layoutMode = "floating", target = null) {
7463
- log18.debug("createIframeMount", { position, initialMode, layoutMode, hasTarget: !!target });
7464
- const iframe = document.createElement("iframe");
7465
- iframe.title = `${BRAND.name} chat`;
7466
- iframe.setAttribute("allow", "microphone; clipboard-write");
7467
- sizeIframe(iframe, position, initialMode, layoutMode);
7468
- const parent = layoutMode === "inline" && target ? target : document.body;
7469
- parent.appendChild(iframe);
7470
- const doc = iframe.contentDocument;
7471
- if (!doc) throw new Error("iframe contentDocument unavailable (cross-origin?)");
7472
- doc.open();
7473
- doc.write(IFRAME_SRCDOC);
7474
- doc.close();
7475
- const mount2 = createShadowMount({ ...opts, target: doc.body });
7476
- const mo = new MutationObserver(() => {
7477
- sizeIframe(iframe, position, mount2.hostElement.dataset.mode ?? "closed", layoutMode);
7478
- });
7479
- mo.observe(mount2.hostElement, { attributes: true, attributeFilter: ["data-mode"] });
7480
- return {
7481
- ...mount2,
7482
- iframeElement: iframe,
7483
- destroy() {
7484
- mo.disconnect();
7485
- mount2.destroy();
7486
- iframe.remove();
7487
- }
7488
- };
7489
- }
7490
- var BASE_CSS = "border:0;background:transparent;z-index:2147483647;pointer-events:auto;";
7491
- var FLOATING_SIZE_BY_MODE = {
7492
- closed: { w: "88px", h: "88px" },
7493
- expanded: { w: "min(calc(100vw - 32px), 680px)", h: "min(calc(100vh - 32px), 860px)" },
7494
- // `open` (normal) — also the fallback for any other non-fullscreen state.
7495
- open: { w: "min(calc(100vw - 32px), 440px)", h: "min(calc(100vh - 32px), 700px)" }
7496
- };
7497
- function sizeIframe(iframe, position, mode, layoutMode) {
7498
- if (mode === "fullscreen" || layoutMode === "standalone") {
7499
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7500
- return;
7501
- }
7502
- if (layoutMode === "inline") {
7503
- iframe.style.cssText = `${BASE_CSS}position:static;display:block;width:100%;height:100%;`;
7504
- return;
7505
- }
7506
- const compact = typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches;
7507
- if (compact && (mode === "open" || mode === "expanded")) {
7508
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7509
- return;
7510
- }
7511
- const vKey = position.startsWith("top-") ? "top" : "bottom";
7512
- const hKey = position.endsWith("-left") ? "left" : "right";
7513
- const { w, h: h2 } = FLOATING_SIZE_BY_MODE[mode] ?? FLOATING_SIZE_BY_MODE.open;
7514
- iframe.style.cssText = `${BASE_CSS}position:fixed;${vKey}:16px;${hKey}:16px;width:${w};height:${h2};`;
7515
- }
7516
-
7517
- // src/core/events.ts
7518
- var EventBus = class {
7519
- constructor() {
7520
- __publicField(this, "handlers", /* @__PURE__ */ new Map());
7521
- }
7522
- /**
7523
- * Register a listener for `name`.
7524
- * @returns an `off` function that removes this listener.
7525
- */
7526
- on(name, fn) {
7527
- let set = this.handlers.get(name);
7528
- if (!set) {
7529
- set = /* @__PURE__ */ new Set();
7530
- this.handlers.set(name, set);
7531
- }
7532
- set.add(fn);
7533
- return () => this.handlers.get(name)?.delete(fn);
7534
- }
7535
- /**
7536
- * Dispatch an event synchronously to all listeners.
7537
- * Listener errors are swallowed — the widget must not crash on user callbacks.
7538
- */
7539
- emit(name, payload) {
7540
- const set = this.handlers.get(name);
7541
- if (!set) return;
7542
- for (const fn of set) {
7543
- try {
7544
- fn(payload);
7545
- } catch {
7546
- }
7547
- }
7548
- }
7549
- /** Remove every listener. Called by the instance `destroy()`. */
7550
- clear() {
7551
- this.handlers.clear();
7552
- }
7553
- };
7554
-
7555
7558
  // src/core/tracking.ts
7556
7559
  var PROTOCOL_VERSION = "1";
7557
7560
  var MAX_HITS_PER_MINUTE = 60;
@@ -7667,6 +7670,73 @@ function createTracker(bus, deps) {
7667
7670
  };
7668
7671
  }
7669
7672
 
7673
+ // src/core/boot.ts
7674
+ function createWidgetRuntime(params) {
7675
+ const { bus, host, appRoot, persistence, reportError } = params;
7676
+ let userRaw = params.initialRaw;
7677
+ let serverConfig;
7678
+ let currentOptions = resolveOptions(userRaw);
7679
+ let firstRender = true;
7680
+ const recompute = () => {
7681
+ currentOptions = resolveOptions(serverConfig ? mergeServerConfig(userRaw, serverConfig) : userRaw);
7682
+ };
7683
+ const applyHostStyling = () => {
7684
+ applyHostPosition(host, hostPositionForMode(currentOptions.mode));
7685
+ const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7686
+ applyHostAttributes(host, cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions);
7687
+ if (firstRender) {
7688
+ applyMode(host, resolveInitialHostMode(currentOptions));
7689
+ firstRender = false;
7690
+ }
7691
+ };
7692
+ const render2 = () => {
7693
+ applyHostStyling();
7694
+ renderPreact(
7695
+ h(ErrorBoundary, { onError: reportError }, h(App, { bus, hostElement: host, options: currentOptions })),
7696
+ appRoot
7697
+ );
7698
+ };
7699
+ bus.on("handshake", (response) => {
7700
+ if (!response.config) return;
7701
+ serverConfig = response.config;
7702
+ recompute();
7703
+ render2();
7704
+ });
7705
+ const untrack = createTracker(bus, {
7706
+ getOptions: () => currentOptions,
7707
+ getVisitorId: () => persistence.getVisitorId(),
7708
+ getConversationId: () => persistence.loadConversationId()
7709
+ });
7710
+ return {
7711
+ getOptions: () => currentOptions,
7712
+ getUserOptions: () => userRaw,
7713
+ render: render2,
7714
+ setUserOptions: (raw) => {
7715
+ userRaw = raw;
7716
+ recompute();
7717
+ render2();
7718
+ },
7719
+ patchUserOptions: (patch) => {
7720
+ userRaw = { ...userRaw, ...patch };
7721
+ recompute();
7722
+ render2();
7723
+ },
7724
+ destroy: () => untrack()
7725
+ };
7726
+ }
7727
+ function resolveInitialHostMode(options) {
7728
+ if (options.mode === "standalone") return "standalone";
7729
+ if (options.mode === "inline") return "inline";
7730
+ const persistence = createPersistence(options.widgetId, options.storage);
7731
+ const { panelOpen, panelSize } = resolveInitialPanelState(
7732
+ options,
7733
+ currentViewportWidth(),
7734
+ persistence.loadPanelOpen(),
7735
+ persistence.loadPanelSize()
7736
+ );
7737
+ return computeHostMode(options.mode, panelSize, panelOpen);
7738
+ }
7739
+
7670
7740
  // src/core/hooks.ts
7671
7741
  var log19 = logger.scope("hooks");
7672
7742
  var HOOK_WILDCARD = "*";
@@ -7752,73 +7822,45 @@ function mount(opts) {
7752
7822
  if (opts.debug !== void 0) setLogLevel(opts.debug);
7753
7823
  const log20 = logger.scope("mount");
7754
7824
  log20.info("mount", { widgetId: opts.widgetId, publicKey: opts.publicKey, mode: opts.presentation?.mode });
7755
- let rawOptions = opts;
7756
- let currentOptions = resolveOptions(rawOptions);
7757
- const widgetId = currentOptions.widgetId;
7825
+ const resolved = resolveOptions(opts);
7826
+ const widgetId = resolved.widgetId;
7758
7827
  const existing = instances.get(widgetId);
7759
7828
  if (existing) {
7760
7829
  log20.debug("destroy existing instance at slot", { widgetId });
7761
7830
  existing.destroy();
7762
7831
  }
7763
- const initialHostMode = resolveInitialHostMode(currentOptions);
7764
- const mountResult = currentOptions.iframe ? createIframeMount(
7765
- { position: hostPositionForMode(currentOptions.mode) },
7766
- currentOptions.position,
7832
+ const initialHostMode = resolveInitialHostMode(resolved);
7833
+ const mountResult = resolved.iframe ? createIframeMount(
7834
+ { position: hostPositionForMode(resolved.mode) },
7835
+ resolved.position,
7767
7836
  initialHostMode,
7768
- currentOptions.mode,
7837
+ resolved.mode,
7769
7838
  opts.target ?? null
7770
7839
  ) : createShadowMount({
7771
- position: hostPositionForMode(currentOptions.mode),
7840
+ position: hostPositionForMode(resolved.mode),
7772
7841
  target: opts.target ?? null
7773
7842
  });
7774
- const persistence = createPersistence(currentOptions.widgetId, currentOptions.storage);
7775
- const applyResolvedHostAttributes = () => {
7776
- const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7777
- applyHostAttributes(
7778
- mountResult.hostElement,
7779
- cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions
7780
- );
7781
- };
7782
- applyResolvedHostAttributes();
7783
- applyMode(mountResult.hostElement, initialHostMode);
7784
7843
  mountResult.hostElement.dataset.widgetId = widgetId;
7785
7844
  const bus = new EventBus();
7845
+ const persistence = createPersistence(widgetId, resolved.storage);
7786
7846
  const reportError = (error) => {
7787
7847
  bus.emit("error", error);
7788
- currentOptions.onError?.(error);
7789
- };
7790
- const renderApp = () => {
7791
- render(
7792
- h(
7793
- ErrorBoundary,
7794
- { onError: reportError },
7795
- h(App, { bus, hostElement: mountResult.hostElement, options: currentOptions })
7796
- ),
7797
- mountResult.appRoot
7798
- );
7848
+ runtime.getOptions().onError?.(error);
7799
7849
  };
7800
- renderApp();
7801
- bus.on("handshake", (response) => {
7802
- if (!response.config) return;
7803
- rawOptions = mergeServerConfig(rawOptions, response.config);
7804
- currentOptions = resolveOptions(rawOptions);
7805
- applyHostPosition(mountResult.hostElement, hostPositionForMode(currentOptions.mode));
7806
- applyResolvedHostAttributes();
7807
- renderApp();
7808
- });
7809
- const unsubscribeTracker = createTracker(bus, {
7810
- getOptions: () => currentOptions,
7811
- getVisitorId: () => persistence.getVisitorId(),
7812
- getConversationId: () => persistence.loadConversationId()
7850
+ const runtime = createWidgetRuntime({
7851
+ bus,
7852
+ host: mountResult.hostElement,
7853
+ appRoot: mountResult.appRoot,
7854
+ persistence,
7855
+ initialRaw: opts,
7856
+ reportError
7813
7857
  });
7858
+ runtime.render();
7814
7859
  const host = mountResult.hostElement;
7815
7860
  let unsubscribeHooks = null;
7816
7861
  const applyUpdate = (patch) => {
7817
7862
  if (patch.debug !== void 0) setLogLevel(patch.debug);
7818
- rawOptions = { ...rawOptions, ...patch };
7819
- currentOptions = resolveOptions(rawOptions);
7820
- applyResolvedHostAttributes();
7821
- renderApp();
7863
+ runtime.patchUserOptions(patch);
7822
7864
  };
7823
7865
  const instance = {
7824
7866
  widgetId,
@@ -7837,7 +7879,7 @@ function mount(opts) {
7837
7879
  // context; App re-handshakes with the SAME visitor/conversation.
7838
7880
  setUserContext: (userContext) => {
7839
7881
  log20.debug("setUserContext", { keys: Object.keys(userContext) });
7840
- applyUpdate({ userContext: { ...rawOptions.userContext, ...userContext } });
7882
+ applyUpdate({ userContext: { ...runtime.getUserOptions().userContext, ...userContext } });
7841
7883
  },
7842
7884
  clearUserContext: () => {
7843
7885
  log20.debug("clearUserContext");
@@ -7845,7 +7887,7 @@ function mount(opts) {
7845
7887
  },
7846
7888
  setPageContext: (pageContext) => {
7847
7889
  log20.debug("setPageContext", { keys: Object.keys(pageContext) });
7848
- applyUpdate({ pageContext: { ...rawOptions.pageContext, ...pageContext } });
7890
+ applyUpdate({ pageContext: { ...runtime.getUserOptions().pageContext, ...pageContext } });
7849
7891
  },
7850
7892
  clearPageContext: () => {
7851
7893
  log20.debug("clearPageContext");
@@ -7860,7 +7902,7 @@ function mount(opts) {
7860
7902
  destroy: () => {
7861
7903
  log20.info("destroy", { widgetId });
7862
7904
  unsubscribeHooks?.();
7863
- unsubscribeTracker();
7905
+ runtime.destroy();
7864
7906
  bus.clear();
7865
7907
  render(null, mountResult.appRoot);
7866
7908
  mountResult.destroy();
@@ -7899,18 +7941,6 @@ function hook(widgetId, event, listener) {
7899
7941
  }
7900
7942
  var version = "0.1.0";
7901
7943
  var brand = BRAND;
7902
- function resolveInitialHostMode(options) {
7903
- if (options.mode === "standalone") return "standalone";
7904
- if (options.mode === "inline") return "inline";
7905
- const persistence = createPersistence(options.widgetId, options.storage);
7906
- const { panelOpen, panelSize } = resolveInitialPanelState(
7907
- options,
7908
- currentViewportWidth(),
7909
- persistence.loadPanelOpen(),
7910
- persistence.loadPanelSize()
7911
- );
7912
- return computeHostMode(options.mode, panelSize, panelOpen);
7913
- }
7914
7944
  export {
7915
7945
  HOOK_WILDCARD,
7916
7946
  brand,