@helpai/elements 0.29.1 → 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 = {
@@ -524,8 +520,9 @@ function resolveOptions(rawOpts) {
524
520
  const header = opts.header ?? {};
525
521
  const { themeMode, themeOverrides } = parseTheme(opts.theme);
526
522
  const mode = presentation.mode ?? "floating";
527
- const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
528
- const availableLocales = i18n.availableLocales ?? [locale];
523
+ const defaultLocale = resolveDefaultLocale(i18n.defaultLocale);
524
+ const availableLocales = i18n.availableLocales ?? [i18n.locale ?? defaultLocale];
525
+ const locale = clampLocale(i18n.locale ?? defaultLocale, availableLocales, defaultLocale);
529
526
  const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
530
527
  const agentApiBaseUrl = (opts.agentApiBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
531
528
  const dataApiBaseUrl = (opts.dataApiBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
@@ -567,6 +564,7 @@ function resolveOptions(rawOpts) {
567
564
  maxCount: opts.composer?.attachments?.maxCount ?? DEFAULT_ATTACHMENTS.maxCount
568
565
  },
569
566
  locale,
567
+ defaultLocale,
570
568
  availableLocales,
571
569
  startMinimized: behavior.startMinimized ?? true,
572
570
  actions: resolveActions(mode, header.actions),
@@ -597,6 +595,11 @@ function resolveDefaultLocale(input) {
597
595
  if (input && input !== "auto") return input;
598
596
  return typeof navigator !== "undefined" && navigator.language ? navigator.language : "en";
599
597
  }
598
+ function clampLocale(desired, availableLocales, defaultLocale) {
599
+ if (availableLocales.length <= 1 || availableLocales.includes(desired)) return desired;
600
+ if (availableLocales.includes(defaultLocale)) return defaultLocale;
601
+ return availableLocales[0] ?? desired;
602
+ }
600
603
  function resolveActions(mode, overrides) {
601
604
  return overrides ?? DEFAULT_ACTIONS_BY_MODE[mode];
602
605
  }
@@ -745,13 +748,14 @@ function resolveLauncher(overrides) {
745
748
 
746
749
  // src/core/config/overrides.ts
747
750
  function applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize) {
748
- if (activeLocale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
751
+ const locale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
752
+ if (locale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
749
753
  return options;
750
754
  }
751
755
  const next = { ...options };
752
- if (activeLocale !== options.locale) {
753
- next.locale = activeLocale;
754
- next.strings = resolveStrings(activeLocale, options.stringsOverride);
756
+ if (locale !== options.locale) {
757
+ next.locale = locale;
758
+ next.strings = resolveStrings(locale, options.stringsOverride);
755
759
  }
756
760
  if (activeThemeMode !== options.themeMode) next.themeMode = activeThemeMode;
757
761
  if (activeTextSize !== options.textSize) next.textSize = activeTextSize;
@@ -797,6 +801,8 @@ function mergeServerConfig(user, server) {
797
801
  serverI18n ?? {},
798
802
  userI18n
799
803
  );
804
+ if (serverI18n?.availableLocales !== void 0) merged.availableLocales = serverI18n.availableLocales;
805
+ if (serverI18n?.defaultLocale !== void 0) merged.defaultLocale = serverI18n.defaultLocale;
800
806
  const userStrings = userI18n?.strings;
801
807
  const serverStrings = serverI18n?.strings;
802
808
  if (userStrings || serverStrings) {
@@ -925,20 +931,6 @@ var REACTIVE_ATTRS = [
925
931
  ...SPECIAL_REACTIVE_ATTRS
926
932
  ];
927
933
 
928
- // src/core/handshake-shape.ts
929
- function isPlainObject(raw) {
930
- return !!raw && typeof raw === "object" && !Array.isArray(raw);
931
- }
932
- function isSiteConfigShape(raw) {
933
- return isPlainObject(raw);
934
- }
935
- function isBlocksConfigShape(raw) {
936
- if (!isPlainObject(raw)) return false;
937
- if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
938
- if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
939
- return true;
940
- }
941
-
942
934
  // src/core/host-commands.ts
943
935
  function commandEventName(cmd) {
944
936
  return `${BRAND.cssPrefix}:${cmd}`;
@@ -959,6 +951,155 @@ function bindHostCommands(host, handlers) {
959
951
  };
960
952
  }
961
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
+
962
1103
  // src/styles/tokens.css
963
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';
964
1105
 
@@ -983,7 +1124,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
983
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);
984
1125
 
985
1126
  // src/shadow/mount.ts
986
- var log2 = logger.scope("mount");
1127
+ var log3 = logger.scope("mount");
987
1128
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
988
1129
  var HOST_CSS = {
989
1130
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1000,7 +1141,7 @@ function hostPositionForMode(mode) {
1000
1141
  }
1001
1142
  function createShadowMount(opts = {}) {
1002
1143
  const position = opts.position ?? "flow";
1003
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1144
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1004
1145
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1005
1146
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1006
1147
  customElements.define(tagName, class extends HTMLElement {
@@ -1084,6 +1225,123 @@ function applyHostAttributes(host, resolved) {
1084
1225
  applySize(host, resolved.size);
1085
1226
  }
1086
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
+
1087
1345
  // src/ui/host-mode.ts
1088
1346
  function computeHostMode(configured, panelSize, isOpen) {
1089
1347
  if (configured === "page") return "page";
@@ -1134,30 +1392,6 @@ function createAuth(opts) {
1134
1392
  };
1135
1393
  }
1136
1394
 
1137
- // src/core/ids.ts
1138
- function uuid7() {
1139
- const ts = Date.now();
1140
- const bytes = new Uint8Array(16);
1141
- bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1142
- bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1143
- bytes[2] = ts >>> 24 & 255;
1144
- bytes[3] = ts >>> 16 & 255;
1145
- bytes[4] = ts >>> 8 & 255;
1146
- bytes[5] = ts & 255;
1147
- fillRandom(bytes.subarray(6));
1148
- bytes[6] = bytes[6] & 15 | 112;
1149
- bytes[8] = bytes[8] & 63 | 128;
1150
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1151
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1152
- }
1153
- function fillRandom(view) {
1154
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1155
- crypto.getRandomValues(view);
1156
- return;
1157
- }
1158
- for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1159
- }
1160
-
1161
1395
  // src/stream/types.ts
1162
1396
  var StreamError = class extends Error {
1163
1397
  constructor(message, code, status, cause) {
@@ -1170,7 +1404,7 @@ var StreamError = class extends Error {
1170
1404
  };
1171
1405
 
1172
1406
  // src/stream/parser.ts
1173
- var log3 = logger.scope("parser");
1407
+ var log5 = logger.scope("parser");
1174
1408
  async function* parseChatStream(response, signal7) {
1175
1409
  if (!response.ok) {
1176
1410
  const text = await response.text().catch(() => "");
@@ -1222,10 +1456,10 @@ ${part}` : part;
1222
1456
  if (!dataPayload || dataPayload === "[DONE]") return null;
1223
1457
  try {
1224
1458
  const chunk = JSON.parse(dataPayload);
1225
- log3.trace("chunk", { type: chunk.type, eventId });
1459
+ log5.trace("chunk", { type: chunk.type, eventId });
1226
1460
  return { chunk, eventId };
1227
1461
  } catch (err) {
1228
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1462
+ log5.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1229
1463
  return null;
1230
1464
  }
1231
1465
  }
@@ -1380,7 +1614,7 @@ function messageToWireParts(m) {
1380
1614
  }
1381
1615
 
1382
1616
  // src/stream/transport.ts
1383
- var log4 = logger.scope("transport");
1617
+ var log6 = logger.scope("transport");
1384
1618
  var MAX_RESUME_ATTEMPTS = 3;
1385
1619
  var RESUME_BACKOFF_MS = 400;
1386
1620
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1422,6 +1656,12 @@ var AgentTransport = class {
1422
1656
  // always sees the visitor's latest context as they navigate.
1423
1657
  __publicField(this, "userContext");
1424
1658
  __publicField(this, "pageContext");
1659
+ // Active BCP-47 locale, carried as the envelope's `locale` on EVERY request
1660
+ // so the backend can localize replies AND the data module can return
1661
+ // localized content + forms. Refreshed via `setLocale` whenever the visitor
1662
+ // switches (no re-handshake needed). Resolved + clamped to the deployment's
1663
+ // availableLocales by `resolveOptions` before it reaches here.
1664
+ __publicField(this, "locale");
1425
1665
  // ---- Data API (content / forms — module-help-ai-data-api) --------------
1426
1666
  //
1427
1667
  // The DATA surfaces live on `dataApiBaseUrl` (default `${baseUrl}/api/data`)
@@ -1447,6 +1687,15 @@ var AgentTransport = class {
1447
1687
  this.userContext = userContext;
1448
1688
  this.pageContext = pageContext;
1449
1689
  }
1690
+ /**
1691
+ * Update the active locale carried on every subsequent request. Call on
1692
+ * mount (before the first handshake) and whenever the user picks a new
1693
+ * locale — the next content / forms / message request reflects it with no
1694
+ * extra round-trip.
1695
+ */
1696
+ setLocale(locale) {
1697
+ this.locale = locale;
1698
+ }
1450
1699
  /**
1451
1700
  * Seed the visitor + conversation identity from persistence BEFORE the first
1452
1701
  * `handshake` resolves. A mount-time `resumeStream()` runs in parallel
@@ -1463,6 +1712,7 @@ var AgentTransport = class {
1463
1712
  const out = {};
1464
1713
  if (this.visitorId) out.visitorId = this.visitorId;
1465
1714
  if (this.conversationId) out.conversationId = this.conversationId;
1715
+ if (this.locale) out.locale = this.locale;
1466
1716
  const context = encodeContext(this.userContext, this.pageContext);
1467
1717
  if (context) out[CONTEXT_PARAM] = context;
1468
1718
  return out;
@@ -1490,13 +1740,13 @@ var AgentTransport = class {
1490
1740
  }
1491
1741
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1492
1742
  async handshake(body) {
1493
- log4.debug("handshake \u2192", {
1743
+ log6.debug("handshake \u2192", {
1494
1744
  visitorId: body.visitorId,
1495
1745
  conversationId: body.conversationId,
1496
1746
  locale: body.locale
1497
1747
  });
1498
1748
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1499
- log4.debug("handshake \u2190", {
1749
+ log6.debug("handshake \u2190", {
1500
1750
  visitorId: res.visitorId,
1501
1751
  conversationId: res.conversationId,
1502
1752
  hasWelcome: !!res.welcome,
@@ -1504,7 +1754,7 @@ var AgentTransport = class {
1504
1754
  rebind: res.rebind
1505
1755
  });
1506
1756
  if (!isHandshakeResponseShape(res)) {
1507
- 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 });
1508
1758
  }
1509
1759
  this.visitorId = res.visitorId ?? body.visitorId;
1510
1760
  this.conversationId = res.conversationId;
@@ -1512,11 +1762,11 @@ var AgentTransport = class {
1512
1762
  }
1513
1763
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1514
1764
  async uploadFile(file) {
1515
- 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 });
1516
1766
  const fd = new FormData();
1517
1767
  fd.append("file", file, file.name);
1518
1768
  const res = await this.postForm(this.uploadPath, fd, "upload");
1519
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1769
+ log6.debug("uploadFile \u2190", { id: res.id, url: res.url });
1520
1770
  return res;
1521
1771
  }
1522
1772
  /**
@@ -1524,12 +1774,12 @@ var AgentTransport = class {
1524
1774
  * Only used when `features.voice === 'server'`.
1525
1775
  */
1526
1776
  async transcribe(audio, mimeType = "audio/webm") {
1527
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1777
+ log6.debug("transcribe \u2192", { size: audio.size, mimeType });
1528
1778
  const fd = new FormData();
1529
1779
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1530
1780
  fd.append("mimeType", mimeType);
1531
1781
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1532
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1782
+ log6.debug("transcribe \u2190", { textLen: res.text.length });
1533
1783
  return res;
1534
1784
  }
1535
1785
  async listConversations(params = {}) {
@@ -1537,14 +1787,14 @@ var AgentTransport = class {
1537
1787
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1538
1788
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1539
1789
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1540
- log4.debug("listConversations \u2192", {
1790
+ log6.debug("listConversations \u2192", {
1541
1791
  visitorId: params.visitorId ?? this.visitorId,
1542
1792
  limit: params.limit,
1543
1793
  cursor: params.cursor
1544
1794
  });
1545
1795
  const res = await this.getJson(url.toString(), "listConversations");
1546
1796
  const conversations = res.conversations ?? [];
1547
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1797
+ log6.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1548
1798
  return {
1549
1799
  conversations: conversations.map((conversation) => ({
1550
1800
  conversationId: conversation.conversationId,
@@ -1560,10 +1810,10 @@ var AgentTransport = class {
1560
1810
  async loadConversation(conversationId) {
1561
1811
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1562
1812
  url.searchParams.set("conversationId", conversationId);
1563
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1813
+ log6.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1564
1814
  const res = await this.getJson(url.toString(), "loadConversation");
1565
1815
  const messages = res.messages ?? [];
1566
- log4.debug("loadConversation \u2190", {
1816
+ log6.debug("loadConversation \u2190", {
1567
1817
  conversationId: res.conversationId,
1568
1818
  canContinue: res.canContinue,
1569
1819
  messageCount: messages.length,
@@ -1585,11 +1835,11 @@ var AgentTransport = class {
1585
1835
  */
1586
1836
  async markRead(conversationId) {
1587
1837
  if (!this.visitorId || !conversationId) return;
1588
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1838
+ log6.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1589
1839
  try {
1590
1840
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1591
1841
  } catch (err) {
1592
- log4.debug("markRead failed (non-fatal)", { err });
1842
+ log6.debug("markRead failed (non-fatal)", { err });
1593
1843
  }
1594
1844
  }
1595
1845
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
@@ -1597,7 +1847,7 @@ var AgentTransport = class {
1597
1847
  const key = JSON.stringify(query, Object.keys(query).toSorted());
1598
1848
  const cached = this.contentCache.get(key);
1599
1849
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1600
- log4.debug("listContent \u2192 cache", query);
1850
+ log6.debug("listContent \u2192 cache", query);
1601
1851
  return cached.result;
1602
1852
  }
1603
1853
  const result = this.fetchContent(query).catch((err) => {
@@ -1613,10 +1863,10 @@ var AgentTransport = class {
1613
1863
  if (value === void 0) continue;
1614
1864
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1615
1865
  }
1616
- log4.debug("listContent \u2192", query);
1866
+ log6.debug("listContent \u2192", query);
1617
1867
  const res = await this.getJson(url.toString(), "listContent");
1618
1868
  const items = res.items ?? [];
1619
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1869
+ log6.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1620
1870
  return { ...res, items };
1621
1871
  }
1622
1872
  /**
@@ -1634,21 +1884,21 @@ var AgentTransport = class {
1634
1884
  async bootstrap(conversationId) {
1635
1885
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1636
1886
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1637
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1887
+ log6.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1638
1888
  const res = await this.getJson(url.toString(), "bootstrap");
1639
1889
  const forms = res.forms ?? [];
1640
1890
  const formSubmissions = res.formSubmissions ?? [];
1641
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1891
+ log6.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1642
1892
  return { forms, formSubmissions };
1643
1893
  }
1644
1894
  async saveUserPrefs(userPrefs) {
1645
- log4.debug("saveUserPrefs \u2192", {
1895
+ log6.debug("saveUserPrefs \u2192", {
1646
1896
  visitorId: this.visitorId,
1647
1897
  conversationId: this.conversationId,
1648
1898
  settingsKeys: Object.keys(userPrefs)
1649
1899
  });
1650
1900
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1651
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1901
+ log6.debug("saveUserPrefs \u2190", { ok: res.ok });
1652
1902
  return res;
1653
1903
  }
1654
1904
  get uploadPath() {
@@ -1669,15 +1919,15 @@ var AgentTransport = class {
1669
1919
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1670
1920
  */
1671
1921
  async submitForm(body) {
1672
- 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 });
1673
1923
  try {
1674
1924
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1675
1925
  } catch (err) {
1676
- log4.debug("submitForm failed (non-fatal)", { err });
1926
+ log6.debug("submitForm failed (non-fatal)", { err });
1677
1927
  }
1678
1928
  }
1679
1929
  sendMessage(body) {
1680
- log4.debug("message \u2192", {
1930
+ log6.debug("message \u2192", {
1681
1931
  conversationId: body.conversationId,
1682
1932
  visitorId: body.visitorId,
1683
1933
  messageCount: body.messages.length,
@@ -1757,17 +2007,17 @@ var AgentTransport = class {
1757
2007
  });
1758
2008
  } catch (err) {
1759
2009
  if (ctrl.signal.aborted) return;
1760
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
2010
+ log6.debug("resume fetch failed \u2014 retrying", { attempt, err });
1761
2011
  continue;
1762
2012
  }
1763
2013
  if (res.status === 204 || !res.ok) {
1764
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
2014
+ log6.debug("resume \u2192 no in-flight stream", { status: res.status });
1765
2015
  return;
1766
2016
  }
1767
2017
  const before = seenIds.size;
1768
2018
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1769
2019
  if (seenIds.size === before) {
1770
- 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");
1771
2021
  return;
1772
2022
  }
1773
2023
  }
@@ -1794,14 +2044,14 @@ var AgentTransport = class {
1794
2044
  } catch (err) {
1795
2045
  if (ctrl.signal.aborted) return true;
1796
2046
  if (err instanceof StreamError && err.status !== void 0) throw err;
1797
- log4.debug("stream segment dropped", { err });
2047
+ log6.debug("stream segment dropped", { err });
1798
2048
  }
1799
2049
  return false;
1800
2050
  }
1801
2051
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1802
2052
  cancelStream(ctrl, conversationId) {
1803
2053
  if (ctrl.signal.aborted) return;
1804
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2054
+ log6.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1805
2055
  ctrl.abort();
1806
2056
  if (!this.visitorId || !conversationId) return;
1807
2057
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1810,7 +2060,7 @@ var AgentTransport = class {
1810
2060
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1811
2061
  body: JSON.stringify(this.withEnvelope({})),
1812
2062
  keepalive: true
1813
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2063
+ }).catch((err) => log6.debug("cancel POST failed (non-fatal)", { err }));
1814
2064
  }
1815
2065
  // ---- Low-level fetch helpers ------------------------------------------
1816
2066
  async *openMessageStream(body, signal7) {
@@ -1892,13 +2142,13 @@ var AgentTransport = class {
1892
2142
  if (attempt >= MAX_REQUEST_RETRIES) {
1893
2143
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1894
2144
  }
1895
- log4.debug("request network error \u2014 retrying", { label, attempt });
2145
+ log6.debug("request network error \u2014 retrying", { label, attempt });
1896
2146
  await sleep(backoffMs(attempt));
1897
2147
  continue;
1898
2148
  }
1899
2149
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1900
2150
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1901
- 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 });
1902
2152
  await sleep(wait);
1903
2153
  }
1904
2154
  }
@@ -2093,7 +2343,7 @@ var TRIGGER = {
2093
2343
  };
2094
2344
 
2095
2345
  // src/stream/reducer.ts
2096
- var log5 = logger.scope("reducer");
2346
+ var log7 = logger.scope("reducer");
2097
2347
  var StreamReducer = class {
2098
2348
  constructor(messagesSig) {
2099
2349
  __publicField(this, "messagesSig", messagesSig);
@@ -2115,13 +2365,13 @@ var StreamReducer = class {
2115
2365
  case "finish-step":
2116
2366
  return;
2117
2367
  case "finish":
2118
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2368
+ log7.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2119
2369
  m.status = "complete";
2120
2370
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2121
2371
  this.bumpDone(m);
2122
2372
  return;
2123
2373
  case "error":
2124
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2374
+ log7.warn("stream error chunk", { errorText: chunk.errorText });
2125
2375
  m.status = "error";
2126
2376
  m.errorText = chunk.errorText;
2127
2377
  this.bumpDone(m);
@@ -2160,226 +2410,101 @@ var StreamReducer = class {
2160
2410
  appendPart(m, { kind: "file", url: chunk.url, mediaType: chunk.mediaType });
2161
2411
  return;
2162
2412
  case "source-url": {
2163
- const parts = m.partsSig.value;
2164
- if (parts.some((p33) => p33.kind === "source" && p33.sourceId === chunk.sourceId)) return;
2165
- appendPart(m, { kind: "source", sourceId: chunk.sourceId, url: chunk.url, title: chunk.title });
2166
- return;
2167
- }
2168
- case "data-notification":
2169
- return;
2170
- default: {
2171
- const _exhaustive = chunk;
2172
- void _exhaustive;
2173
- }
2174
- }
2175
- }
2176
- /**
2177
- * Terminal-state bump (`finish`/`error`). Touches the message's own `partsSig`
2178
- * AND the list so subscribers re-render: a buffered bubble (subscribed to its
2179
- * parts) flips out of the loading indicator into the finished reply, and the
2180
- * list reflects the new `status`.
2181
- */
2182
- bumpDone(m) {
2183
- m.partsSig.value = [...m.partsSig.value];
2184
- this.messagesSig.value = [...this.messagesSig.value];
2185
- }
2186
- };
2187
- function ensureTextPart(m, kind, id) {
2188
- const existing = m.partsSig.value.find((p33) => p33.kind === kind && p33.id === id);
2189
- if (existing) return existing;
2190
- const part = { kind, id, textSig: signal2(""), doneSig: signal2(false) };
2191
- appendPart(m, part);
2192
- return part;
2193
- }
2194
- function ensureToolPart(m, toolCallId, toolName) {
2195
- const existing = m.partsSig.value.find((p33) => p33.kind === "tool" && p33.toolCallId === toolCallId);
2196
- if (existing) return existing;
2197
- const part = {
2198
- kind: "tool",
2199
- toolCallId,
2200
- toolName: toolName ?? "tool",
2201
- inputPartialSig: signal2(""),
2202
- inputSig: signal2(void 0),
2203
- outputSig: signal2(void 0),
2204
- errorSig: signal2(void 0),
2205
- stateSig: signal2("input"),
2206
- approvalSig: signal2(void 0)
2207
- };
2208
- appendPart(m, part);
2209
- return part;
2210
- }
2211
- function appendPart(m, part) {
2212
- m.partsSig.value = [...m.partsSig.value, part];
2213
- }
2214
- function applyTool(m, chunk) {
2215
- switch (chunk.type) {
2216
- case "tool-input-start":
2217
- ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2218
- return;
2219
- case "tool-input-delta": {
2220
- const p33 = ensureToolPart(m, chunk.toolCallId);
2221
- p33.inputPartialSig.value += chunk.delta;
2222
- return;
2223
- }
2224
- case "tool-input-available": {
2225
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2226
- p33.inputSig.value = chunk.input;
2227
- p33.stateSig.value = isAskUserInputTool(p33.toolName) ? "awaiting-input" : "awaiting";
2228
- return;
2229
- }
2230
- case "tool-approval-request": {
2231
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2232
- p33.approvalSig.value = { id: chunk.approvalId };
2233
- p33.stateSig.value = "awaiting-approval";
2234
- return;
2235
- }
2236
- case "tool-output-available": {
2237
- const p33 = ensureToolPart(m, chunk.toolCallId);
2238
- p33.outputSig.value = chunk.output;
2239
- p33.stateSig.value = "output";
2240
- return;
2241
- }
2242
- case "tool-output-error": {
2243
- const p33 = ensureToolPart(m, chunk.toolCallId);
2244
- p33.errorSig.value = chunk.errorText;
2245
- p33.stateSig.value = "error";
2246
- return;
2247
- }
2248
- default:
2249
- return;
2250
- }
2251
- }
2252
-
2253
- // src/core/persistence.ts
2254
- var log6 = logger.scope("persistence");
2255
- function createPersistence(widgetId, storage = defaultStorage) {
2256
- const prefix = `${BRAND.cssPrefix}.${widgetId}`;
2257
- const KEY_VISITOR = `${prefix}.visitorId`;
2258
- const KEY_CONVERSATION = `${prefix}.conversationId`;
2259
- const KEY_USER_PREFS = `${prefix}.userPrefs`;
2260
- const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
2261
- const KEY_PANEL_SIZE = `${prefix}.panelSize`;
2262
- const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
2263
- const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
2264
- const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
2265
- const KEY_FORMS_DONE = `${prefix}.formsDone`;
2266
- const persistence = {
2267
- getVisitorId() {
2268
- const existing = storage.get(KEY_VISITOR);
2269
- if (existing) return existing;
2270
- const minted = uuid7();
2271
- storage.set(KEY_VISITOR, minted);
2272
- return minted;
2273
- },
2274
- setVisitorId(id) {
2275
- log6.debug("setVisitorId (rebind)", { id, widgetId });
2276
- storage.set(KEY_VISITOR, id);
2277
- },
2278
- loadConversationId() {
2279
- return storage.get(KEY_CONVERSATION) ?? void 0;
2280
- },
2281
- saveConversationId(id) {
2282
- log6.trace("saveConversationId", { id, widgetId });
2283
- if (id) storage.set(KEY_CONVERSATION, id);
2284
- else storage.remove(KEY_CONVERSATION);
2285
- },
2286
- loadUserPrefs() {
2287
- const raw = storage.get(KEY_USER_PREFS);
2288
- if (!raw) return {};
2289
- try {
2290
- const parsed = JSON.parse(raw);
2291
- return isPlainObject2(parsed) ? parsed : {};
2292
- } catch (error) {
2293
- log6.warn("loadUserPrefs parse failed", { error });
2294
- return {};
2295
- }
2296
- },
2297
- saveUserPrefs(s) {
2298
- try {
2299
- storage.set(KEY_USER_PREFS, JSON.stringify(s));
2300
- } catch (error) {
2301
- log6.warn("saveUserPrefs serialise failed", { error });
2302
- }
2303
- },
2304
- patchUserPrefs(patch) {
2305
- const current = persistence.loadUserPrefs();
2306
- persistence.saveUserPrefs({ ...current, ...patch });
2307
- },
2308
- loadPanelOpen() {
2309
- const raw = storage.get(KEY_PANEL_OPEN);
2310
- if (raw === "1") return true;
2311
- if (raw === "0") return false;
2312
- return null;
2313
- },
2314
- savePanelOpen(open) {
2315
- storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
2316
- },
2317
- loadPanelSize() {
2318
- const raw = storage.get(KEY_PANEL_SIZE);
2319
- return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
2320
- },
2321
- savePanelSize(size) {
2322
- storage.set(KEY_PANEL_SIZE, size);
2323
- },
2324
- loadCalloutDismissed(currentText) {
2325
- if (!currentText) return false;
2326
- const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
2327
- return !!dismissedText && dismissedText === currentText;
2328
- },
2329
- saveCalloutDismissed(currentText) {
2330
- if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
2331
- else storage.remove(KEY_CALLOUT_DISMISSED);
2332
- },
2333
- loadSidebarCollapsed() {
2334
- const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
2335
- if (raw === "1") return true;
2336
- if (raw === "0") return false;
2337
- return null;
2338
- },
2339
- saveSidebarCollapsed(collapsed) {
2340
- storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
2341
- },
2342
- loadActiveModule() {
2343
- return storage.get(KEY_ACTIVE_MODULE) || null;
2344
- },
2345
- saveActiveModule(id) {
2346
- storage.set(KEY_ACTIVE_MODULE, id);
2347
- },
2348
- loadFormsDone() {
2349
- const raw = storage.get(KEY_FORMS_DONE);
2350
- if (!raw) return {};
2351
- try {
2352
- const parsed = JSON.parse(raw);
2353
- return isPlainObject2(parsed) ? parsed : {};
2354
- } catch (error) {
2355
- log6.warn("loadFormsDone parse failed", { error });
2356
- return {};
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;
2357
2417
  }
2358
- },
2359
- saveFormDone(id) {
2360
- try {
2361
- const done = persistence.loadFormsDone();
2362
- done[id] = Date.now();
2363
- storage.set(KEY_FORMS_DONE, JSON.stringify(done));
2364
- } catch (error) {
2365
- log6.warn("saveFormDone serialise failed", { error });
2418
+ case "data-notification":
2419
+ return;
2420
+ default: {
2421
+ const _exhaustive = chunk;
2422
+ void _exhaustive;
2366
2423
  }
2367
- },
2368
- clearConversation() {
2369
- storage.remove(KEY_CONVERSATION);
2370
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)
2371
2457
  };
2372
- return persistence;
2458
+ appendPart(m, part);
2459
+ return part;
2373
2460
  }
2374
- function isPlainObject2(v) {
2375
- 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
+ }
2376
2501
  }
2377
2502
 
2378
2503
  // src/core/feedback/index.ts
2379
2504
  import { signal as signal3 } from "@preact/signals";
2380
2505
 
2381
2506
  // src/core/feedback/audio.ts
2382
- var log7 = logger.scope("feedback:audio");
2507
+ var log8 = logger.scope("feedback:audio");
2383
2508
  var AudioEngine = class {
2384
2509
  constructor() {
2385
2510
  __publicField(this, "ctx", null);
@@ -2400,7 +2525,7 @@ var AudioEngine = class {
2400
2525
  this.master.gain.value = 1;
2401
2526
  this.master.connect(this.ctx.destination);
2402
2527
  } catch (err) {
2403
- log7.warn("AudioContext init failed", { err });
2528
+ log8.warn("AudioContext init failed", { err });
2404
2529
  }
2405
2530
  }
2406
2531
  /** Route all sounds through this multiplier. Call on volume change. */
@@ -2457,7 +2582,7 @@ var AudioEngine = class {
2457
2582
  this.buffers.set(url, buf);
2458
2583
  return buf;
2459
2584
  }).catch((err) => {
2460
- log7.warn("audio fetch/decode failed", { url, err });
2585
+ log8.warn("audio fetch/decode failed", { url, err });
2461
2586
  this.buffers.delete(url);
2462
2587
  return null;
2463
2588
  });
@@ -2543,7 +2668,7 @@ function acquireHaptics() {
2543
2668
  }
2544
2669
 
2545
2670
  // src/core/feedback/index.ts
2546
- var log8 = logger.scope("feedback");
2671
+ var log9 = logger.scope("feedback");
2547
2672
  var DEDUP_WINDOW_MS = 100;
2548
2673
  var FeedbackBus = class {
2549
2674
  constructor(sound, haptics) {
@@ -2621,7 +2746,7 @@ var FeedbackBus = class {
2621
2746
  const override = this.sound.events?.[event];
2622
2747
  if (override === false) return;
2623
2748
  if (typeof override === "string") {
2624
- 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 }));
2625
2750
  return;
2626
2751
  }
2627
2752
  const tone = DEFAULT_TONES[event];
@@ -3266,7 +3391,7 @@ function AgentBadge({ agent }) {
3266
3391
  import { useEffect as useEffect4, useRef as useRef2, useState as useState2 } from "preact/hooks";
3267
3392
 
3268
3393
  // src/core/attachments.ts
3269
- var log9 = logger.scope("attachments");
3394
+ var log10 = logger.scope("attachments");
3270
3395
  function ingest(source, existing, limits) {
3271
3396
  const incoming = collectFiles(source);
3272
3397
  const accepted = [];
@@ -3285,7 +3410,7 @@ function ingest(source, existing, limits) {
3285
3410
  totalCount += 1;
3286
3411
  }
3287
3412
  if (accepted.length || rejected.length) {
3288
- log9.debug("ingest", {
3413
+ log10.debug("ingest", {
3289
3414
  accepted: accepted.length,
3290
3415
  rejected: rejected.map((r) => ({ name: r.file.name, reason: r.reason }))
3291
3416
  });
@@ -3296,7 +3421,7 @@ function revoke(att) {
3296
3421
  try {
3297
3422
  URL.revokeObjectURL(att.previewUrl);
3298
3423
  } catch (error) {
3299
- log9.debug("revokeObjectURL failed", { error });
3424
+ log10.debug("revokeObjectURL failed", { error });
3300
3425
  }
3301
3426
  }
3302
3427
  function reject(file, count, bytes, limits, accept) {
@@ -3472,7 +3597,7 @@ function pickSupportedMimeType() {
3472
3597
  // src/ui/composer.tsx
3473
3598
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
3474
3599
  var p7 = BRAND.cssPrefix;
3475
- var log10 = logger.scope("composer");
3600
+ var log11 = logger.scope("composer");
3476
3601
  function totalBytes(items) {
3477
3602
  return items.reduce((acc, a) => acc + a.size, 0);
3478
3603
  }
@@ -3525,7 +3650,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3525
3650
  attachFromDrop: (items) => {
3526
3651
  const result = ingest(items, attsRef.current, options.attachments);
3527
3652
  if (result.accepted.length === 0) return;
3528
- 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) });
3529
3654
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3530
3655
  setAtts((prev) => [...prev, ...result.accepted]);
3531
3656
  }
@@ -3546,7 +3671,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3546
3671
  const result = ingest(data.items, attsRef.current, options.attachments);
3547
3672
  if (result.accepted.length > 0) {
3548
3673
  e.preventDefault();
3549
- 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) });
3550
3675
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3551
3676
  setAtts((prev) => [...prev, ...result.accepted]);
3552
3677
  return;
@@ -3580,14 +3705,14 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3580
3705
  const input = e.target;
3581
3706
  const result = ingest(input.files, atts, options.attachments);
3582
3707
  if (result.accepted.length > 0) {
3583
- 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) });
3584
3709
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3585
3710
  setAtts((prev) => [...prev, ...result.accepted]);
3586
3711
  }
3587
3712
  input.value = "";
3588
3713
  };
3589
3714
  const removeAtt = (id) => {
3590
- log10.info("removeAttachment", { id });
3715
+ log11.info("removeAttachment", { id });
3591
3716
  bus.emit("removeAttachment", { id });
3592
3717
  setAtts((prev) => {
3593
3718
  const target = prev.find((a) => a.id === id);
@@ -3600,7 +3725,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3600
3725
  if (!voice.isSupported) return;
3601
3726
  if (voiceOn) {
3602
3727
  const durationMs = voiceStartedAtRef.current ? Date.now() - voiceStartedAtRef.current : 0;
3603
- log10.info("voiceStop", { durationMs });
3728
+ log11.info("voiceStop", { durationMs });
3604
3729
  bus.emit("voiceStop", { durationMs });
3605
3730
  voice.stop();
3606
3731
  setVoiceOn(false);
@@ -3609,7 +3734,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3609
3734
  return;
3610
3735
  }
3611
3736
  voiceStartedAtRef.current = Date.now();
3612
- log10.info("voiceStart");
3737
+ log11.info("voiceStart");
3613
3738
  bus.emit("voiceStart", void 0);
3614
3739
  setVoiceOn(true);
3615
3740
  feedback.play("voiceStart");
@@ -3619,7 +3744,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3619
3744
  setVoiceOn(false);
3620
3745
  if (voiceStartedAtRef.current) {
3621
3746
  const durationMs = Date.now() - voiceStartedAtRef.current;
3622
- log10.debug("voiceStop (auto)", { durationMs });
3747
+ log11.debug("voiceStop (auto)", { durationMs });
3623
3748
  bus.emit("voiceStop", { durationMs });
3624
3749
  voiceStartedAtRef.current = null;
3625
3750
  }
@@ -5180,7 +5305,7 @@ function startOfDay(ms) {
5180
5305
 
5181
5306
  // src/ui/conversation-list.tsx
5182
5307
  import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs15 } from "preact/jsx-runtime";
5183
- var log11 = logger.scope("history");
5308
+ var log12 = logger.scope("history");
5184
5309
  var BUCKET_TO_STRING = {
5185
5310
  today: "dateToday",
5186
5311
  yesterday: "dateYesterday",
@@ -5213,7 +5338,7 @@ function ConversationList({
5213
5338
  setState("loaded");
5214
5339
  }).catch((err) => {
5215
5340
  if (cancelled) return;
5216
- log11.warn("listConversations failed", { err });
5341
+ log12.warn("listConversations failed", { err });
5217
5342
  setState("error");
5218
5343
  });
5219
5344
  return () => {
@@ -5856,7 +5981,7 @@ function ModuleState({
5856
5981
  // src/ui/modules/help.tsx
5857
5982
  import { jsx as jsx27, jsxs as jsxs23 } from "preact/jsx-runtime";
5858
5983
  var p23 = BRAND.cssPrefix;
5859
- var log12 = logger.scope("help");
5984
+ var log13 = logger.scope("help");
5860
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 });
5861
5986
  function groupByCategory(items) {
5862
5987
  const groups = /* @__PURE__ */ new Map();
@@ -5910,7 +6035,7 @@ function HelpRoot({ transport, strings, config, nav, panelProps }) {
5910
6035
  setState("loaded");
5911
6036
  }).catch((err) => {
5912
6037
  if (cancelled) return;
5913
- log12.warn("listContent (help) failed", { err });
6038
+ log13.warn("listContent (help) failed", { err });
5914
6039
  setErrorMsg(errorMessageFor(err, strings));
5915
6040
  setState("error");
5916
6041
  });
@@ -5961,7 +6086,7 @@ function HomeCard({ onClick, children, testid }) {
5961
6086
  // src/ui/modules/home.tsx
5962
6087
  import { Fragment as Fragment6, jsx as jsx29, jsxs as jsxs24 } from "preact/jsx-runtime";
5963
6088
  var p25 = BRAND.cssPrefix;
5964
- var log13 = logger.scope("home");
6089
+ var log14 = logger.scope("home");
5965
6090
  function resolveGreeting(props2) {
5966
6091
  const name = props2.options.userContext?.name;
5967
6092
  const fromConfig = props2.config.greetingText;
@@ -5980,7 +6105,7 @@ function HomeRoot(props2) {
5980
6105
  useEffect12(() => {
5981
6106
  if (!config.showRecentConversations) return;
5982
6107
  let cancelled = false;
5983
- 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 }));
5984
6109
  return () => {
5985
6110
  cancelled = true;
5986
6111
  };
@@ -5988,7 +6113,7 @@ function HomeRoot(props2) {
5988
6113
  useEffect12(() => {
5989
6114
  if (!config.contentTags?.length) return;
5990
6115
  let cancelled = false;
5991
- 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 }));
5992
6117
  return () => {
5993
6118
  cancelled = true;
5994
6119
  };
@@ -6062,7 +6187,7 @@ var homeLayout = {
6062
6187
  import { useEffect as useEffect13, useState as useState10 } from "preact/hooks";
6063
6188
  import { jsx as jsx30, jsxs as jsxs25 } from "preact/jsx-runtime";
6064
6189
  var p26 = BRAND.cssPrefix;
6065
- var log14 = logger.scope("news");
6190
+ var log15 = logger.scope("news");
6066
6191
  function NewsRoot({ transport, strings, config, nav, panelProps }) {
6067
6192
  const tags = config.contentTags;
6068
6193
  const [state, setState] = useState10("loading");
@@ -6078,7 +6203,7 @@ function NewsRoot({ transport, strings, config, nav, panelProps }) {
6078
6203
  setState("loaded");
6079
6204
  }).catch((err) => {
6080
6205
  if (cancelled) return;
6081
- log14.warn("listContent (news) failed", { err });
6206
+ log15.warn("listContent (news) failed", { err });
6082
6207
  setErrorMsg(errorMessageFor(err, strings));
6083
6208
  setState("error");
6084
6209
  });
@@ -6193,7 +6318,7 @@ function IframeView({ url, title, strings, onBack, actions }) {
6193
6318
  import { useCallback as useCallback3, useEffect as useEffect14, useState as useState11 } from "preact/hooks";
6194
6319
  import { jsx as jsx33, jsxs as jsxs28 } from "preact/jsx-runtime";
6195
6320
  var p29 = BRAND.cssPrefix;
6196
- var log15 = logger.scope("content");
6321
+ var log16 = logger.scope("content");
6197
6322
  function ContentView({ id, title, transport, strings, onBack, actions }) {
6198
6323
  const [item, setItem] = useState11(null);
6199
6324
  const [failed, setFailed] = useState11(false);
@@ -6212,7 +6337,7 @@ function ContentView({ id, title, transport, strings, onBack, actions }) {
6212
6337
  else setFailed(true);
6213
6338
  }).catch((err) => {
6214
6339
  if (cancelled) return;
6215
- log15.warn("listContent (reader) failed", { err });
6340
+ log16.warn("listContent (reader) failed", { err });
6216
6341
  setFailed(true);
6217
6342
  });
6218
6343
  return () => {
@@ -6432,7 +6557,7 @@ function useLauncherCallout({ callout, persistence }) {
6432
6557
 
6433
6558
  // src/ui/app.tsx
6434
6559
  import { jsx as jsx36, jsxs as jsxs31 } from "preact/jsx-runtime";
6435
- var log16 = logger.scope("app");
6560
+ var log17 = logger.scope("app");
6436
6561
  var p32 = BRAND.cssPrefix;
6437
6562
  function App({ options, hostElement, bus }) {
6438
6563
  const [persistence] = useState13(() => createPersistence(options.widgetId, options.storage));
@@ -6490,7 +6615,7 @@ function App({ options, hostElement, bus }) {
6490
6615
  });
6491
6616
  const calloutPersistent = options.launcher.callout?.persistent ?? true;
6492
6617
  const dismissCallout = useCallback6(() => {
6493
- log16.info("calloutDismiss");
6618
+ log17.info("calloutDismiss");
6494
6619
  bus.emit("calloutDismiss", void 0);
6495
6620
  dismissCalloutRaw();
6496
6621
  }, [bus, dismissCalloutRaw]);
@@ -6524,7 +6649,7 @@ function App({ options, hostElement, bus }) {
6524
6649
  (patch) => {
6525
6650
  persistence.patchUserPrefs(patch);
6526
6651
  transport.saveUserPrefs(persistence.loadUserPrefs()).catch((err) => {
6527
- log16.warn("saveUserPrefs failed", { err });
6652
+ log17.warn("saveUserPrefs failed", { err });
6528
6653
  });
6529
6654
  },
6530
6655
  [persistence, transport]
@@ -6605,7 +6730,7 @@ function App({ options, hostElement, bus }) {
6605
6730
  values: rec.values
6606
6731
  }));
6607
6732
  } catch (err) {
6608
- log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6733
+ log17.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6609
6734
  return [];
6610
6735
  }
6611
6736
  },
@@ -6645,7 +6770,7 @@ function App({ options, hostElement, bus }) {
6645
6770
  }
6646
6771
  } catch (err) {
6647
6772
  if (isStale()) return;
6648
- log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
6773
+ log17.warn("loadThread failed; resetting conversationId", { err, conversationId });
6649
6774
  conversationIdSig.value = void 0;
6650
6775
  persistence.saveConversationId(void 0);
6651
6776
  } finally {
@@ -6670,7 +6795,10 @@ function App({ options, hostElement, bus }) {
6670
6795
  visitorId,
6671
6796
  conversationId,
6672
6797
  userPrefs: persistence.loadUserPrefs(),
6673
- locale: options.locale
6798
+ // The visitor's ACTIVE locale (persisted pick, else auto-detected) —
6799
+ // not the config baseline — so the handshake learns what the visitor
6800
+ // wants. The response's availableLocales then clamps it for the rest.
6801
+ locale: activeLocale
6674
6802
  });
6675
6803
  } catch (err) {
6676
6804
  if (isStale()) return;
@@ -6681,7 +6809,7 @@ function App({ options, hostElement, bus }) {
6681
6809
  }
6682
6810
  if (isStale()) return;
6683
6811
  if (res.visitorId && res.visitorId !== visitorId) {
6684
- 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 });
6685
6813
  persistence.setVisitorId(res.visitorId);
6686
6814
  setVisitorId(res.visitorId);
6687
6815
  bus.emit("visitorRebound", {
@@ -6691,7 +6819,7 @@ function App({ options, hostElement, bus }) {
6691
6819
  });
6692
6820
  }
6693
6821
  if (res.conversationId && res.conversationId !== conversationId) {
6694
- log16.info("conversation rebound", {
6822
+ log17.info("conversation rebound", {
6695
6823
  previous: conversationId,
6696
6824
  current: res.conversationId,
6697
6825
  reason: res.rebind?.conversationId
@@ -6706,9 +6834,9 @@ function App({ options, hostElement, bus }) {
6706
6834
  if (res.welcome?.suggestions) setSuggestions(res.welcome.suggestions);
6707
6835
  if (options.mode === "page") {
6708
6836
  if (res.site && isSiteConfigShape(res.site)) setParsedSite(res.site);
6709
- 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 });
6710
6838
  if (res.blocks && isBlocksConfigShape(res.blocks)) setParsedBlocks(res.blocks);
6711
- 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 });
6712
6840
  }
6713
6841
  if (res.userPrefs && !newConversation) {
6714
6842
  persistence.saveUserPrefs(res.userPrefs);
@@ -6732,7 +6860,18 @@ function App({ options, hostElement, bus }) {
6732
6860
  setConversationReady(true);
6733
6861
  }
6734
6862
  },
6735
- [transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
6863
+ [
6864
+ transport,
6865
+ visitorId,
6866
+ persistence,
6867
+ options,
6868
+ bus,
6869
+ conversationIdSig,
6870
+ feedback,
6871
+ playWelcome,
6872
+ loadThread,
6873
+ activeLocale
6874
+ ]
6736
6875
  );
6737
6876
  const userSig = JSON.stringify(options.userContext ?? null);
6738
6877
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6741,6 +6880,13 @@ function App({ options, hostElement, bus }) {
6741
6880
  const merged = Object.keys(formContext).length ? { ...formContext, ...options.userContext } : options.userContext;
6742
6881
  transport.setContext(merged, toWirePageContext(options.pageContext));
6743
6882
  }, [transport, userSig, pageSig, formCtxSig]);
6883
+ const effectiveLocale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
6884
+ useEffect17(() => {
6885
+ transport.setLocale(effectiveLocale);
6886
+ }, [transport, effectiveLocale]);
6887
+ useEffect17(() => {
6888
+ if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
6889
+ }, [effectiveLocale, activeLocale]);
6744
6890
  const runResume = useCallback6(
6745
6891
  async (handle) => {
6746
6892
  let bubble = null;
@@ -6756,7 +6902,7 @@ function App({ options, hostElement, bus }) {
6756
6902
  resumeActiveRef.current = true;
6757
6903
  const chatTab = chatTabIdRef.current;
6758
6904
  if (chatTab) homeNav.switchTab(chatTab);
6759
- log16.info("resumed in-flight reply on mount");
6905
+ log17.info("resumed in-flight reply on mount");
6760
6906
  }
6761
6907
  reducer.apply(evt.chunk);
6762
6908
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) setCanSend(false);
@@ -6772,7 +6918,7 @@ function App({ options, hostElement, bus }) {
6772
6918
  bubble.errorText = errorMessageFor(err, options.strings);
6773
6919
  feedback.play("error");
6774
6920
  }
6775
- log16.debug("mount resume ended without completing", { err });
6921
+ log17.debug("mount resume ended without completing", { err });
6776
6922
  } finally {
6777
6923
  resumeBubbleRef.current = null;
6778
6924
  if (bubble) {
@@ -6803,7 +6949,7 @@ function App({ options, hostElement, bus }) {
6803
6949
  const res = await dataBootRef.current;
6804
6950
  setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
6805
6951
  } catch (err) {
6806
- log16.debug("bootstrap failed \u2014 treating as no forms", { err });
6952
+ log17.debug("bootstrap failed \u2014 treating as no forms", { err });
6807
6953
  } finally {
6808
6954
  setFormsReady(true);
6809
6955
  }
@@ -6903,7 +7049,7 @@ function App({ options, hostElement, bus }) {
6903
7049
  (pt) => pt.kind === "tool" && pt.toolCallId === toolCallId
6904
7050
  );
6905
7051
  if (!part) {
6906
- log16.warn("resolveTool: tool part not found", { toolCallId });
7052
+ log17.warn("resolveTool: tool part not found", { toolCallId });
6907
7053
  return;
6908
7054
  }
6909
7055
  mutate(part);
@@ -6917,7 +7063,7 @@ function App({ options, hostElement, bus }) {
6917
7063
  );
6918
7064
  const handleToolResult = useCallback6(
6919
7065
  (toolCallId, output) => {
6920
- log16.info("toolResult", { toolCallId });
7066
+ log17.info("toolResult", { toolCallId });
6921
7067
  bus.emit("toolResult", { toolCallId });
6922
7068
  resolveTool(toolCallId, TRIGGER.toolResult, (part) => {
6923
7069
  part.outputSig.value = output;
@@ -6928,7 +7074,7 @@ function App({ options, hostElement, bus }) {
6928
7074
  );
6929
7075
  const handleToolDecision = useCallback6(
6930
7076
  (toolCallId, approved, reason) => {
6931
- log16.info("toolDecision", { toolCallId, approved });
7077
+ log17.info("toolDecision", { toolCallId, approved });
6932
7078
  bus.emit("toolDecision", { toolCallId, approved });
6933
7079
  resolveTool(toolCallId, TRIGGER.toolApproval, (part) => {
6934
7080
  const id = part.approvalSig.value?.id ?? toolCallId;
@@ -6952,7 +7098,7 @@ function App({ options, hostElement, bus }) {
6952
7098
  pageArea: () => options.pageContext?.area ? String(options.pageContext.area) : void 0,
6953
7099
  conversationId: () => conversationIdSig.value,
6954
7100
  onComplete: (form, trigger, values, skipped) => {
6955
- log16.info("formSubmit", { formId: form.id, trigger, skipped });
7101
+ log17.info("formSubmit", { formId: form.id, trigger, skipped });
6956
7102
  bus.emit("formSubmit", { formId: form.id, values, skipped });
6957
7103
  setFormMarkers((prev) => [
6958
7104
  ...prev.filter((m) => !(m.formId === form.id && m.outcome === "skipped")),
@@ -6997,12 +7143,12 @@ function App({ options, hostElement, bus }) {
6997
7143
  }, [idleMs, isOpen, forms, msgCount.value]);
6998
7144
  const handleSend = useCallback6(
6999
7145
  async (text, attachments) => {
7000
- log16.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7146
+ log17.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7001
7147
  if (streaming) return;
7002
7148
  bus.emit("send", { text, attachmentCount: attachments.length });
7003
7149
  if (suggestions.length > 0) setSuggestions([]);
7004
7150
  const uploaded = await uploadAttachments(transport, attachments, (err) => {
7005
- log16.warn("upload failed", { err });
7151
+ log17.warn("upload failed", { err });
7006
7152
  bus.emit("error", err);
7007
7153
  options.onError?.(err);
7008
7154
  });
@@ -7019,24 +7165,24 @@ function App({ options, hostElement, bus }) {
7019
7165
  [streaming, transport, bus, options, suggestions.length, messagesSig, reducer, streamAssistant, forms]
7020
7166
  );
7021
7167
  const handleStop = useCallback6(() => {
7022
- log16.info("stop");
7168
+ log17.info("stop");
7023
7169
  bus.emit("stop", void 0);
7024
7170
  activeCancel?.();
7025
7171
  }, [activeCancel, bus]);
7026
7172
  const handleSoundToggle = useCallback6(() => {
7027
7173
  feedback.toggleMuted();
7028
7174
  const muted = feedback.mutedSig.value;
7029
- log16.info("soundToggle", { muted });
7175
+ log17.info("soundToggle", { muted });
7030
7176
  bus.emit("soundToggle", { muted });
7031
7177
  }, [feedback, bus]);
7032
7178
  const handleClose = useCallback6(() => {
7033
7179
  if (resolveCloseAction(options.mode, panelSize) === "exit-fullscreen") {
7034
- log16.info("close \u2192 exit-fullscreen", { mode: options.mode });
7180
+ log17.info("close \u2192 exit-fullscreen", { mode: options.mode });
7035
7181
  setPanelSize("normal");
7036
7182
  bus.emit("fullscreen", false);
7037
7183
  return;
7038
7184
  }
7039
- log16.info("close \u2192 panel closed", { mode: options.mode });
7185
+ log17.info("close \u2192 panel closed", { mode: options.mode });
7040
7186
  forms.fire("panel-close");
7041
7187
  setIsOpen(false);
7042
7188
  persistence.savePanelOpen(false);
@@ -7047,7 +7193,7 @@ function App({ options, hostElement, bus }) {
7047
7193
  if (!options.modules.list.some((m) => m.layout === "chat")) return;
7048
7194
  transport.listConversations({ limit: 50 }).then((res) => {
7049
7195
  unreadCountSig.value = res.conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0);
7050
- }).catch((err) => log16.debug("refreshUnread failed (non-fatal)", { err }));
7196
+ }).catch((err) => log17.debug("refreshUnread failed (non-fatal)", { err }));
7051
7197
  }, [transport, options.modules, unreadCountSig]);
7052
7198
  const unreadSeeded = useRef9(false);
7053
7199
  useEffect17(() => {
@@ -7056,7 +7202,7 @@ function App({ options, hostElement, bus }) {
7056
7202
  refreshUnread();
7057
7203
  }, [activated, conversationReady, refreshUnread]);
7058
7204
  const handleOpen = useCallback6(() => {
7059
- log16.info("open", { mode: options.mode });
7205
+ log17.info("open", { mode: options.mode });
7060
7206
  feedback.unlock();
7061
7207
  setLauncherLeaving(true);
7062
7208
  setIsOpen(true);
@@ -7070,11 +7216,11 @@ function App({ options, hostElement, bus }) {
7070
7216
  const handleClear = useCallback6(() => {
7071
7217
  const hasUserMessages = messagesSig.value.some((m) => m.role === "user");
7072
7218
  if (!hasUserMessages && canSend && conversationIdSig.value) {
7073
- log16.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7219
+ log17.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7074
7220
  if (messagesSig.value.length === 0) playWelcome(welcomeRef.current?.messages);
7075
7221
  return;
7076
7222
  }
7077
- log16.info("clear \u2192 new conversation", { wasStreaming: streaming });
7223
+ log17.info("clear \u2192 new conversation", { wasStreaming: streaming });
7078
7224
  bus.emit("clear", void 0);
7079
7225
  if (streaming) activeCancel?.();
7080
7226
  messagesSig.value = [];
@@ -7092,14 +7238,14 @@ function App({ options, hostElement, bus }) {
7092
7238
  }, [handleClear, homeNav, options]);
7093
7239
  const handleExpand = useCallback6(() => {
7094
7240
  const nextSize = panelSize === "expanded" ? "normal" : "expanded";
7095
- log16.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7241
+ log17.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7096
7242
  setPanelSize(nextSize);
7097
7243
  persistence.savePanelSize(nextSize);
7098
7244
  bus.emit("expand", nextSize === "expanded");
7099
7245
  }, [bus, panelSize, persistence]);
7100
7246
  const handleFullscreen = useCallback6(() => {
7101
7247
  const nextSize = panelSize === "fullscreen" ? "normal" : "fullscreen";
7102
- log16.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7248
+ log17.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7103
7249
  setPanelSize(nextSize);
7104
7250
  persistence.savePanelSize(nextSize);
7105
7251
  bus.emit("fullscreen", nextSize === "fullscreen");
@@ -7109,21 +7255,21 @@ function App({ options, hostElement, bus }) {
7109
7255
  const url = new URL(options.popOutUrl);
7110
7256
  if (conversationIdSig.value) url.searchParams.set("conversation", conversationIdSig.value);
7111
7257
  if (options.widgetId !== "default") url.searchParams.set("widgetId", options.widgetId);
7112
- log16.info("popOut", { url: url.toString() });
7258
+ log17.info("popOut", { url: url.toString() });
7113
7259
  window.open(url.toString(), "_blank", "noopener,noreferrer");
7114
7260
  bus.emit("popOut", void 0);
7115
7261
  }, [bus, conversationIdSig, options.popOutUrl, options.widgetId]);
7116
7262
  const handleToggleHistory = useCallback6(() => {
7117
7263
  setView((v) => {
7118
7264
  const next = v === "history" ? "chat" : "history";
7119
- log16.info("toggleHistory", { view: next });
7265
+ log17.info("toggleHistory", { view: next });
7120
7266
  bus.emit("toggleHistory", { view: next });
7121
7267
  return next;
7122
7268
  });
7123
7269
  }, [bus]);
7124
7270
  const handleLocaleChange = useCallback6(
7125
7271
  (locale) => {
7126
- log16.info("localeChange", { locale });
7272
+ log17.info("localeChange", { locale });
7127
7273
  setActiveLocale(locale);
7128
7274
  patchAndSync({ locale });
7129
7275
  bus.emit("localeChange", locale);
@@ -7132,7 +7278,7 @@ function App({ options, hostElement, bus }) {
7132
7278
  );
7133
7279
  const handleThemeChange = useCallback6(
7134
7280
  (mode) => {
7135
- log16.info("themeChange", { mode });
7281
+ log17.info("themeChange", { mode });
7136
7282
  setActiveThemeMode(mode);
7137
7283
  hostElement.dataset.theme = mode;
7138
7284
  patchAndSync({ themeMode: mode });
@@ -7142,7 +7288,7 @@ function App({ options, hostElement, bus }) {
7142
7288
  );
7143
7289
  const handleTextSizeChange = useCallback6(
7144
7290
  (size) => {
7145
- log16.info("textSizeChange", { size });
7291
+ log17.info("textSizeChange", { size });
7146
7292
  setActiveTextSize(size);
7147
7293
  hostElement.dataset.textSize = size;
7148
7294
  patchAndSync({ textSize: size });
@@ -7153,7 +7299,7 @@ function App({ options, hostElement, bus }) {
7153
7299
  const handleToggleSidebarCollapsed = useCallback6(() => {
7154
7300
  setSidebarCollapsed((prev) => {
7155
7301
  const next = !prev;
7156
- log16.info("sidebarToggle", { collapsed: next });
7302
+ log17.info("sidebarToggle", { collapsed: next });
7157
7303
  persistence.saveSidebarCollapsed(next);
7158
7304
  bus.emit("sidebarToggle", { collapsed: next });
7159
7305
  return next;
@@ -7161,7 +7307,7 @@ function App({ options, hostElement, bus }) {
7161
7307
  }, [persistence, bus]);
7162
7308
  const handleWidgetSizeChange = useCallback6(
7163
7309
  (size) => {
7164
- log16.info("resize", size);
7310
+ log17.info("resize", size);
7165
7311
  patchAndSync({ widgetSize: size });
7166
7312
  bus.emit("resize", size);
7167
7313
  },
@@ -7169,7 +7315,7 @@ function App({ options, hostElement, bus }) {
7169
7315
  );
7170
7316
  const handleSelectHistoryConversation = useCallback6(
7171
7317
  async (targetConversationId) => {
7172
- log16.info("selectConversation", { conversationId: targetConversationId });
7318
+ log17.info("selectConversation", { conversationId: targetConversationId });
7173
7319
  bus.emit("selectConversation", { conversationId: targetConversationId });
7174
7320
  try {
7175
7321
  const [res, markers] = await Promise.all([
@@ -7271,7 +7417,7 @@ function App({ options, hostElement, bus }) {
7271
7417
  onStop: handleStop,
7272
7418
  onSuggestion: (s) => {
7273
7419
  const text = s.text ?? s.label;
7274
- log16.info("suggestion", { text });
7420
+ log17.info("suggestion", { text });
7275
7421
  bus.emit("suggestion", { text });
7276
7422
  void handleSend(text, []);
7277
7423
  },
@@ -7393,14 +7539,14 @@ function emitMessage(bus, options, role, text) {
7393
7539
 
7394
7540
  // src/ui/error-boundary.tsx
7395
7541
  import { Component } from "preact";
7396
- var log17 = logger.scope("error-boundary");
7542
+ var log18 = logger.scope("error-boundary");
7397
7543
  var ErrorBoundary = class extends Component {
7398
7544
  constructor() {
7399
7545
  super(...arguments);
7400
7546
  __publicField(this, "state", { crashed: false });
7401
7547
  }
7402
7548
  componentDidCatch(error, info) {
7403
- log17.error("widget render crashed", { error, componentStack: info?.componentStack });
7549
+ log18.error("widget render crashed", { error, componentStack: info?.componentStack });
7404
7550
  this.setState({ crashed: true });
7405
7551
  this.props.onError(error, info?.componentStack);
7406
7552
  }
@@ -7409,102 +7555,6 @@ var ErrorBoundary = class extends Component {
7409
7555
  }
7410
7556
  };
7411
7557
 
7412
- // src/shadow/iframe-mount.ts
7413
- var log18 = logger.scope("iframe");
7414
- 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>';
7415
- function createIframeMount(opts, position, initialMode = "closed", layoutMode = "floating", target = null) {
7416
- log18.debug("createIframeMount", { position, initialMode, layoutMode, hasTarget: !!target });
7417
- const iframe = document.createElement("iframe");
7418
- iframe.title = `${BRAND.name} chat`;
7419
- iframe.setAttribute("allow", "microphone; clipboard-write");
7420
- sizeIframe(iframe, position, initialMode, layoutMode);
7421
- const parent = layoutMode === "inline" && target ? target : document.body;
7422
- parent.appendChild(iframe);
7423
- const doc = iframe.contentDocument;
7424
- if (!doc) throw new Error("iframe contentDocument unavailable (cross-origin?)");
7425
- doc.open();
7426
- doc.write(IFRAME_SRCDOC);
7427
- doc.close();
7428
- const mount2 = createShadowMount({ ...opts, target: doc.body });
7429
- const mo = new MutationObserver(() => {
7430
- sizeIframe(iframe, position, mount2.hostElement.dataset.mode ?? "closed", layoutMode);
7431
- });
7432
- mo.observe(mount2.hostElement, { attributes: true, attributeFilter: ["data-mode"] });
7433
- return {
7434
- ...mount2,
7435
- iframeElement: iframe,
7436
- destroy() {
7437
- mo.disconnect();
7438
- mount2.destroy();
7439
- iframe.remove();
7440
- }
7441
- };
7442
- }
7443
- var BASE_CSS = "border:0;background:transparent;z-index:2147483647;pointer-events:auto;";
7444
- var FLOATING_SIZE_BY_MODE = {
7445
- closed: { w: "88px", h: "88px" },
7446
- expanded: { w: "min(calc(100vw - 32px), 680px)", h: "min(calc(100vh - 32px), 860px)" },
7447
- // `open` (normal) — also the fallback for any other non-fullscreen state.
7448
- open: { w: "min(calc(100vw - 32px), 440px)", h: "min(calc(100vh - 32px), 700px)" }
7449
- };
7450
- function sizeIframe(iframe, position, mode, layoutMode) {
7451
- if (mode === "fullscreen" || layoutMode === "standalone") {
7452
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7453
- return;
7454
- }
7455
- if (layoutMode === "inline") {
7456
- iframe.style.cssText = `${BASE_CSS}position:static;display:block;width:100%;height:100%;`;
7457
- return;
7458
- }
7459
- const compact = typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches;
7460
- if (compact && (mode === "open" || mode === "expanded")) {
7461
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7462
- return;
7463
- }
7464
- const vKey = position.startsWith("top-") ? "top" : "bottom";
7465
- const hKey = position.endsWith("-left") ? "left" : "right";
7466
- const { w, h: h2 } = FLOATING_SIZE_BY_MODE[mode] ?? FLOATING_SIZE_BY_MODE.open;
7467
- iframe.style.cssText = `${BASE_CSS}position:fixed;${vKey}:16px;${hKey}:16px;width:${w};height:${h2};`;
7468
- }
7469
-
7470
- // src/core/events.ts
7471
- var EventBus = class {
7472
- constructor() {
7473
- __publicField(this, "handlers", /* @__PURE__ */ new Map());
7474
- }
7475
- /**
7476
- * Register a listener for `name`.
7477
- * @returns an `off` function that removes this listener.
7478
- */
7479
- on(name, fn) {
7480
- let set = this.handlers.get(name);
7481
- if (!set) {
7482
- set = /* @__PURE__ */ new Set();
7483
- this.handlers.set(name, set);
7484
- }
7485
- set.add(fn);
7486
- return () => this.handlers.get(name)?.delete(fn);
7487
- }
7488
- /**
7489
- * Dispatch an event synchronously to all listeners.
7490
- * Listener errors are swallowed — the widget must not crash on user callbacks.
7491
- */
7492
- emit(name, payload) {
7493
- const set = this.handlers.get(name);
7494
- if (!set) return;
7495
- for (const fn of set) {
7496
- try {
7497
- fn(payload);
7498
- } catch {
7499
- }
7500
- }
7501
- }
7502
- /** Remove every listener. Called by the instance `destroy()`. */
7503
- clear() {
7504
- this.handlers.clear();
7505
- }
7506
- };
7507
-
7508
7558
  // src/core/tracking.ts
7509
7559
  var PROTOCOL_VERSION = "1";
7510
7560
  var MAX_HITS_PER_MINUTE = 60;
@@ -7620,6 +7670,73 @@ function createTracker(bus, deps) {
7620
7670
  };
7621
7671
  }
7622
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
+
7623
7740
  // src/core/hooks.ts
7624
7741
  var log19 = logger.scope("hooks");
7625
7742
  var HOOK_WILDCARD = "*";
@@ -7705,73 +7822,45 @@ function mount(opts) {
7705
7822
  if (opts.debug !== void 0) setLogLevel(opts.debug);
7706
7823
  const log20 = logger.scope("mount");
7707
7824
  log20.info("mount", { widgetId: opts.widgetId, publicKey: opts.publicKey, mode: opts.presentation?.mode });
7708
- let rawOptions = opts;
7709
- let currentOptions = resolveOptions(rawOptions);
7710
- const widgetId = currentOptions.widgetId;
7825
+ const resolved = resolveOptions(opts);
7826
+ const widgetId = resolved.widgetId;
7711
7827
  const existing = instances.get(widgetId);
7712
7828
  if (existing) {
7713
7829
  log20.debug("destroy existing instance at slot", { widgetId });
7714
7830
  existing.destroy();
7715
7831
  }
7716
- const initialHostMode = resolveInitialHostMode(currentOptions);
7717
- const mountResult = currentOptions.iframe ? createIframeMount(
7718
- { position: hostPositionForMode(currentOptions.mode) },
7719
- currentOptions.position,
7832
+ const initialHostMode = resolveInitialHostMode(resolved);
7833
+ const mountResult = resolved.iframe ? createIframeMount(
7834
+ { position: hostPositionForMode(resolved.mode) },
7835
+ resolved.position,
7720
7836
  initialHostMode,
7721
- currentOptions.mode,
7837
+ resolved.mode,
7722
7838
  opts.target ?? null
7723
7839
  ) : createShadowMount({
7724
- position: hostPositionForMode(currentOptions.mode),
7840
+ position: hostPositionForMode(resolved.mode),
7725
7841
  target: opts.target ?? null
7726
7842
  });
7727
- const persistence = createPersistence(currentOptions.widgetId, currentOptions.storage);
7728
- const applyResolvedHostAttributes = () => {
7729
- const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7730
- applyHostAttributes(
7731
- mountResult.hostElement,
7732
- cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions
7733
- );
7734
- };
7735
- applyResolvedHostAttributes();
7736
- applyMode(mountResult.hostElement, initialHostMode);
7737
7843
  mountResult.hostElement.dataset.widgetId = widgetId;
7738
7844
  const bus = new EventBus();
7845
+ const persistence = createPersistence(widgetId, resolved.storage);
7739
7846
  const reportError = (error) => {
7740
7847
  bus.emit("error", error);
7741
- currentOptions.onError?.(error);
7742
- };
7743
- const renderApp = () => {
7744
- render(
7745
- h(
7746
- ErrorBoundary,
7747
- { onError: reportError },
7748
- h(App, { bus, hostElement: mountResult.hostElement, options: currentOptions })
7749
- ),
7750
- mountResult.appRoot
7751
- );
7848
+ runtime.getOptions().onError?.(error);
7752
7849
  };
7753
- renderApp();
7754
- bus.on("handshake", (response) => {
7755
- if (!response.config) return;
7756
- rawOptions = mergeServerConfig(rawOptions, response.config);
7757
- currentOptions = resolveOptions(rawOptions);
7758
- applyHostPosition(mountResult.hostElement, hostPositionForMode(currentOptions.mode));
7759
- applyResolvedHostAttributes();
7760
- renderApp();
7761
- });
7762
- const unsubscribeTracker = createTracker(bus, {
7763
- getOptions: () => currentOptions,
7764
- getVisitorId: () => persistence.getVisitorId(),
7765
- getConversationId: () => persistence.loadConversationId()
7850
+ const runtime = createWidgetRuntime({
7851
+ bus,
7852
+ host: mountResult.hostElement,
7853
+ appRoot: mountResult.appRoot,
7854
+ persistence,
7855
+ initialRaw: opts,
7856
+ reportError
7766
7857
  });
7858
+ runtime.render();
7767
7859
  const host = mountResult.hostElement;
7768
7860
  let unsubscribeHooks = null;
7769
7861
  const applyUpdate = (patch) => {
7770
7862
  if (patch.debug !== void 0) setLogLevel(patch.debug);
7771
- rawOptions = { ...rawOptions, ...patch };
7772
- currentOptions = resolveOptions(rawOptions);
7773
- applyResolvedHostAttributes();
7774
- renderApp();
7863
+ runtime.patchUserOptions(patch);
7775
7864
  };
7776
7865
  const instance = {
7777
7866
  widgetId,
@@ -7790,7 +7879,7 @@ function mount(opts) {
7790
7879
  // context; App re-handshakes with the SAME visitor/conversation.
7791
7880
  setUserContext: (userContext) => {
7792
7881
  log20.debug("setUserContext", { keys: Object.keys(userContext) });
7793
- applyUpdate({ userContext: { ...rawOptions.userContext, ...userContext } });
7882
+ applyUpdate({ userContext: { ...runtime.getUserOptions().userContext, ...userContext } });
7794
7883
  },
7795
7884
  clearUserContext: () => {
7796
7885
  log20.debug("clearUserContext");
@@ -7798,7 +7887,7 @@ function mount(opts) {
7798
7887
  },
7799
7888
  setPageContext: (pageContext) => {
7800
7889
  log20.debug("setPageContext", { keys: Object.keys(pageContext) });
7801
- applyUpdate({ pageContext: { ...rawOptions.pageContext, ...pageContext } });
7890
+ applyUpdate({ pageContext: { ...runtime.getUserOptions().pageContext, ...pageContext } });
7802
7891
  },
7803
7892
  clearPageContext: () => {
7804
7893
  log20.debug("clearPageContext");
@@ -7813,7 +7902,7 @@ function mount(opts) {
7813
7902
  destroy: () => {
7814
7903
  log20.info("destroy", { widgetId });
7815
7904
  unsubscribeHooks?.();
7816
- unsubscribeTracker();
7905
+ runtime.destroy();
7817
7906
  bus.clear();
7818
7907
  render(null, mountResult.appRoot);
7819
7908
  mountResult.destroy();
@@ -7852,18 +7941,6 @@ function hook(widgetId, event, listener) {
7852
7941
  }
7853
7942
  var version = "0.1.0";
7854
7943
  var brand = BRAND;
7855
- function resolveInitialHostMode(options) {
7856
- if (options.mode === "standalone") return "standalone";
7857
- if (options.mode === "inline") return "inline";
7858
- const persistence = createPersistence(options.widgetId, options.storage);
7859
- const { panelOpen, panelSize } = resolveInitialPanelState(
7860
- options,
7861
- currentViewportWidth(),
7862
- persistence.loadPanelOpen(),
7863
- persistence.loadPanelSize()
7864
- );
7865
- return computeHostMode(options.mode, panelSize, panelOpen);
7866
- }
7867
7944
  export {
7868
7945
  HOOK_WILDCARD,
7869
7946
  brand,