@helpai/elements 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-component.mjs CHANGED
@@ -19,7 +19,12 @@ var BRAND = {
19
19
  // and @goai builds.
20
20
  tagName: true ? "web-ai-chat" : "web-ai-chat",
21
21
  cssPrefix: true ? "helpai" : "helpai",
22
- defaultBaseUrl: true ? "https://help.ai/api" : "",
22
+ /**
23
+ * MAIN site origin (e.g. `https://help.ai`) — module bases derive from it:
24
+ * agent API at `${defaultBaseUrl}/api/agent`, data API at
25
+ * `${defaultBaseUrl}/api/data`.
26
+ */
27
+ defaultBaseUrl: true ? "https://help.ai" : "",
23
28
  standaloneUrl: true ? "https://help.ai/chat" : "",
24
29
  /** Public-facing brand label, e.g. "Help AI" / "Go AI". */
25
30
  displayName: true ? "Help AI" : "Help AI",
@@ -457,13 +462,12 @@ var DEFAULT_FEATURES = {
457
462
  var DEFAULT_FORMS = { list: [], byTrigger: {} };
458
463
  var DEFAULT_TRACKING = {
459
464
  enabled: false,
460
- endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
461
465
  sampleRate: 1
462
466
  };
463
467
  var DEFAULT_ENDPOINTS = {
464
468
  upload: "/ai/agent/upload-file",
465
469
  transcribe: "/ai/agent/transcribe-audio",
466
- submitForm: "/ai/agent/submit-form"
470
+ submitForm: "/submit-form"
467
471
  };
468
472
  var DEFAULT_LAUNCHER = {
469
473
  variant: "circle",
@@ -505,11 +509,14 @@ function resolveOptions(rawOpts) {
505
509
  const mode = presentation.mode ?? "floating";
506
510
  const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
507
511
  const availableLocales = i18n.availableLocales ?? [locale];
512
+ const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
513
+ const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
508
514
  return {
509
515
  widgetId: opts.widgetId ?? "default",
510
516
  publicKey: opts.publicKey,
511
517
  aiAgentDeploymentId: opts.aiAgentDeploymentId,
512
- baseUrl: (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, ""),
518
+ baseUrl,
519
+ dataBaseUrl,
513
520
  userContext: sanitizeUserContext(opts.userContext),
514
521
  pageContext: sanitizePageContext(opts.pageContext),
515
522
  position: presentation.position ?? "bottom-right",
@@ -554,7 +561,7 @@ function resolveOptions(rawOpts) {
554
561
  poweredBy: resolvePoweredBy(footer.poweredBy),
555
562
  composer: resolveComposer(opts.composer),
556
563
  modules: resolveModules(opts.modules),
557
- tracking: resolveTracking(opts.tracking),
564
+ tracking: resolveTracking(opts.tracking, dataBaseUrl),
558
565
  storage: opts.storage ?? defaultStorage,
559
566
  onMessage: opts.onMessage,
560
567
  onOpen: opts.onOpen,
@@ -587,11 +594,13 @@ function resolveHaptics(overrides) {
587
594
  events: overrides?.events
588
595
  };
589
596
  }
590
- function resolveTracking(overrides) {
597
+ function resolveTracking(overrides, dataBaseUrl) {
591
598
  const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
592
599
  return {
593
600
  enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
594
- endpoint: overrides?.endpoint ?? DEFAULT_TRACKING.endpoint,
601
+ // The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
602
+ // (e.g. `https://help.ai/api/data/elements/px.gif`).
603
+ endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
595
604
  // Clamp instead of reject — a malformed rate must never turn a
596
605
  // disabled tracker on or crash resolve (no Zod on the IIFE path).
597
606
  sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
@@ -665,7 +674,9 @@ function resolveForms(overrides) {
665
674
  submitLabel: def.submitLabel,
666
675
  skippable: def.skippable ?? false,
667
676
  frequency: def.frequency ?? "once",
668
- mirrorToContext: def.mirrorToContext ?? true
677
+ mirrorToContext: def.mirrorToContext ?? true,
678
+ reopenable: def.reopenable ?? true,
679
+ reviewable: def.reviewable ?? true
669
680
  });
670
681
  }
671
682
  const byTrigger = {};
@@ -758,7 +769,7 @@ function mergeServerConfig(user, server) {
758
769
  out[key] = mergeLeaves(sv, uv);
759
770
  }
760
771
  if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
761
- if (server.forms !== void 0 && user.forms === void 0) out.forms = server.forms;
772
+ if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
762
773
  const userI18n = user.i18n;
763
774
  const serverI18n = server.i18n;
764
775
  if (userI18n || serverI18n) {
@@ -818,6 +829,7 @@ var EMBED_SCALAR_ATTRS = [
818
829
  ["public-key", "publicKey"],
819
830
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
820
831
  ["base-url", "baseUrl"],
832
+ ["data-base-url", "dataBaseUrl"],
821
833
  ["theme", "theme", (v) => v]
822
834
  ];
823
835
  var PRESENTATION_ATTRS = [
@@ -1323,16 +1335,33 @@ var DEFAULT_PATHS = {
1323
1335
  * on the client; a failure just leaves the badge until the next sync.
1324
1336
  */
1325
1337
  markRead: "/ai/agent/mark-read",
1326
- // ── CMS content (unified Home / Help / News / docs) ─────────────
1327
- /** All widget content via one filtered endpoint. `GET /ai/agent/content?tags=…`. */
1328
- content: "/ai/agent/content",
1338
+ // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1339
+ /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1340
+ content: "/content",
1341
+ /**
1342
+ * Resolved form definitions for the deployment. `GET /forms` →
1343
+ * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1344
+ * start-conversation); form gating waits for it. A failure is treated as
1345
+ * "no forms" (non-fatal).
1346
+ */
1347
+ forms: "/forms",
1329
1348
  /**
1330
1349
  * Form submission (the event-driven forms engine). POST `{ visitorId,
1331
1350
  * conversationId, formId, trigger, values, skipped? }` → record the form's answers
1332
1351
  * immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
1333
1352
  * Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
1334
1353
  */
1335
- submitForm: "/ai/agent/submit-form"
1354
+ submitForm: "/submit-form",
1355
+ /**
1356
+ * Visitor interaction records for a conversation. `GET
1357
+ * /activity?visitorId=…&conversationId=…` →
1358
+ * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1359
+ * the former `formSubmissions` echo on start-conversation / list-messages;
1360
+ * deliberately generic — future visitor interaction records ride the same
1361
+ * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1362
+ * deployment doesn't resolve) is treated as "no records" (non-fatal).
1363
+ */
1364
+ activity: "/activity"
1336
1365
  };
1337
1366
  var CONTEXT_PARAM = "context";
1338
1367
  function buildSendMessageRequest(params) {
@@ -1605,14 +1634,17 @@ var AgentTransport = class {
1605
1634
  log4.debug("markRead failed (non-fatal)", { err });
1606
1635
  }
1607
1636
  }
1608
- // ---- CMS content (unified) --------------------------------------------
1637
+ // ---- Data API (content / forms — module-help-ai-data-api) --------------
1609
1638
  //
1610
- // One endpoint for every content surface (Home / Help / News / docs).
1611
- // Callers pass filters; a thrown StreamError (e.g. 404 on an older backend)
1612
- // is treated as "no content" by the module and the tab degrades gracefully.
1613
- /** Fetch content rows by filter. `GET /ai/agent/content`. */
1639
+ // The DATA surfaces live on `dataBaseUrl` (default `${baseUrl}/api/data`)
1640
+ // with ROOT-level paths same auth headers + envelope, same retry
1641
+ // behavior. One endpoint for every content surface (Home / Help / News /
1642
+ // docs); callers pass filters; a thrown StreamError (e.g. 404 on an older
1643
+ // backend) is treated as "no content" by the module and the tab degrades
1644
+ // gracefully.
1645
+ /** Fetch content rows by filter. `GET /content` (data-API). */
1614
1646
  async listContent(query = {}) {
1615
- const url = new URL(this.url(DEFAULT_PATHS.content));
1647
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
1616
1648
  for (const [key, value] of Object.entries(query)) {
1617
1649
  if (value === void 0) continue;
1618
1650
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
@@ -1623,6 +1655,40 @@ var AgentTransport = class {
1623
1655
  log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1624
1656
  return { ...res, items };
1625
1657
  }
1658
+ /**
1659
+ * Fetch the deployment's resolved form definitions. `GET /forms`
1660
+ * (data-API). Replaces the former `config.forms` push on the handshake —
1661
+ * called once at boot, in parallel with `startConversation`; form gating
1662
+ * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1663
+ * "no forms" (non-fatal).
1664
+ */
1665
+ async listForms() {
1666
+ log4.debug("listForms \u2192");
1667
+ const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
1668
+ const forms = res.forms ?? [];
1669
+ log4.debug("listForms \u2190", { count: forms.length });
1670
+ return { forms };
1671
+ }
1672
+ /**
1673
+ * Fetch the visitor's recorded activity for a conversation — currently the
1674
+ * form submissions that rebuild the collapsed "form submitted / skipped"
1675
+ * timeline markers on resume. `GET /activity` (data-API); the
1676
+ * envelope carries `visitorId` / `conversationId` as query params.
1677
+ * `conversationId` here is an optional resource selector (a thread that may
1678
+ * differ from the active conversation — the history-pane switch); when
1679
+ * omitted, the envelope's active conversation applies. Callers treat a
1680
+ * thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
1681
+ * "no activity" (non-fatal).
1682
+ */
1683
+ async getActivity(conversationId) {
1684
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
1685
+ if (conversationId) url.searchParams.set("conversationId", conversationId);
1686
+ log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
1687
+ const res = await this.getJson(url.toString(), "getActivity");
1688
+ const formSubmissions = res.formSubmissions ?? [];
1689
+ log4.debug("getActivity \u2190", { count: formSubmissions.length });
1690
+ return { formSubmissions };
1691
+ }
1626
1692
  async saveUserPrefs(userPrefs) {
1627
1693
  log4.debug("saveUserPrefs \u2192", {
1628
1694
  visitorId: this.visitorId,
@@ -1643,8 +1709,9 @@ var AgentTransport = class {
1643
1709
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
1644
1710
  }
1645
1711
  /**
1646
- * Record a completed form (intake, CSAT, claim, …). Fire-and-forget — the
1647
- * record is captured even if the visitor never sends a message. `conversationId`,
1712
+ * Record a completed form (intake, CSAT, claim, …). `POST
1713
+ * /submit-form` (data-API). Fire-and-forget the record is
1714
+ * captured even if the visitor never sends a message. `conversationId`,
1648
1715
  * `formId`, and `trigger` are explicit so the backend can tie + route;
1649
1716
  * `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
1650
1717
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
@@ -1652,7 +1719,7 @@ var AgentTransport = class {
1652
1719
  async submitForm(body) {
1653
1720
  log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1654
1721
  try {
1655
- await this.postJson(this.submitFormPath, body, "submitForm");
1722
+ await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1656
1723
  } catch (err) {
1657
1724
  log4.debug("submitForm failed (non-fatal)", { err });
1658
1725
  }
@@ -1888,9 +1955,24 @@ var AgentTransport = class {
1888
1955
  if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
1889
1956
  return await res.json();
1890
1957
  }
1958
+ /**
1959
+ * Agent-API URL — resolves a `/ai/agent/*` path against the agent module
1960
+ * base, `${baseUrl}/api/agent`. Absolute URLs pass through unchanged.
1961
+ */
1891
1962
  url(pathOrUrl) {
1892
1963
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1893
- return `${this.opts.baseUrl}${pathOrUrl}`;
1964
+ return `${this.opts.baseUrl}/api/agent${pathOrUrl}`;
1965
+ }
1966
+ /**
1967
+ * Data-API variant of {@link url} — resolves a root-level path (content /
1968
+ * forms / submit-form / activity) against `dataBaseUrl`. Absolute URLs
1969
+ * (e.g. an `endpoints.submitForm` pointing straight at a CRM) pass through
1970
+ * unchanged; with no `dataBaseUrl` configured the data module derives from
1971
+ * the main origin: `${baseUrl}/api/data`.
1972
+ */
1973
+ dataUrl(pathOrUrl) {
1974
+ if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1975
+ return `${this.opts.dataBaseUrl ?? `${this.opts.baseUrl}/api/data`}${pathOrUrl}`;
1894
1976
  }
1895
1977
  get fetchImpl() {
1896
1978
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -4357,7 +4439,8 @@ function FormDoneMarker({
4357
4439
  "\xB7 ",
4358
4440
  marker.title
4359
4441
  ] }) : null;
4360
- const values = !skipped && marker.values && Object.keys(marker.values).length > 0 ? marker.values : null;
4442
+ const reviewable = marker.form?.reviewable ?? false;
4443
+ const values = !skipped && reviewable && marker.values && Object.keys(marker.values).length > 0 ? marker.values : null;
4361
4444
  if (values) {
4362
4445
  const labelFor = (name) => marker.form?.fields.find((f) => f.name === name)?.label ?? name;
4363
4446
  return /* @__PURE__ */ jsxs9("details", { class: `${p11}-form-done`, "data-outcome": marker.outcome, "data-testid": TID.formDone, children: [
@@ -4377,7 +4460,7 @@ function FormDoneMarker({
4377
4460
  skipped ? /* @__PURE__ */ jsx11(SkipIcon, {}) : /* @__PURE__ */ jsx11(CheckIcon, {}),
4378
4461
  /* @__PURE__ */ jsx11("span", { children: skipped ? strings.formSkipped : strings.formSubmitted }),
4379
4462
  title,
4380
- skipped && marker.form && onFill ? /* @__PURE__ */ jsx11("button", { type: "button", class: `${p11}-form-done-fill`, "data-testid": TID.formFill, onClick: onFill, children: strings.formFillOut }) : null
4463
+ skipped && marker.form?.reopenable && onFill ? /* @__PURE__ */ jsx11("button", { type: "button", class: `${p11}-form-done-fill`, "data-testid": TID.formFill, onClick: onFill, children: strings.formFillOut }) : null
4381
4464
  ] });
4382
4465
  }
4383
4466
 
@@ -6414,6 +6497,8 @@ function App({ options, hostElement, bus }) {
6414
6497
  const chatTabIdRef = useRef9(void 0);
6415
6498
  chatTabIdRef.current = chatTabId();
6416
6499
  const [conversationReady, setConversationReady] = useState13(false);
6500
+ const [remoteForms, setRemoteForms] = useState13(null);
6501
+ const [formsReady, setFormsReady] = useState13(false);
6417
6502
  const isInlineLike = options.mode === "standalone" || options.mode === "inline";
6418
6503
  const initialPanelRef = useRef9(
6419
6504
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
@@ -6446,6 +6531,7 @@ function App({ options, hostElement, bus }) {
6446
6531
  const [transport] = useState13(
6447
6532
  () => new AgentTransport({
6448
6533
  baseUrl: options.baseUrl,
6534
+ dataBaseUrl: options.dataBaseUrl,
6449
6535
  auth: createAuth({
6450
6536
  publicKey: options.publicKey,
6451
6537
  aiAgentDeploymentId: options.aiAgentDeploymentId,
@@ -6526,6 +6612,26 @@ function App({ options, hostElement, bus }) {
6526
6612
  },
6527
6613
  [messagesSig]
6528
6614
  );
6615
+ const fetchFormMarkers = useCallback6(
6616
+ async (conversationId) => {
6617
+ try {
6618
+ const { formSubmissions: subs } = await transport.getActivity(conversationId);
6619
+ const lastIndexByForm = /* @__PURE__ */ new Map();
6620
+ subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6621
+ return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6622
+ key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6623
+ formId: rec.formId,
6624
+ outcome: rec.skipped ? "skipped" : "submitted",
6625
+ createdAt: parseWireDate(rec.createdAt),
6626
+ values: rec.values
6627
+ }));
6628
+ } catch (err) {
6629
+ log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
6630
+ return [];
6631
+ }
6632
+ },
6633
+ [transport]
6634
+ );
6529
6635
  const connectGenRef = useRef9(0);
6530
6636
  const runStartConversation = useCallback6(
6531
6637
  async ({ newConversation }) => {
@@ -6594,24 +6700,14 @@ function App({ options, hostElement, bus }) {
6594
6700
  if (isResume) {
6595
6701
  setLoadingMessages(true);
6596
6702
  try {
6597
- const chat = res.messages?.length ? {
6703
+ const chatPromise = res.messages?.length ? Promise.resolve({
6598
6704
  conversationId: persistedChatId,
6599
6705
  canContinue: res.canContinue ?? true,
6600
- messages: res.messages,
6601
- formSubmissions: res.formSubmissions
6602
- } : await transport.loadConversation(persistedChatId);
6706
+ messages: res.messages
6707
+ }) : transport.loadConversation(persistedChatId);
6708
+ const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6603
6709
  if (isStale()) return;
6604
6710
  const loaded = (chat.messages ?? []).map(fromWireMessage);
6605
- const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
6606
- const lastIndexByForm = /* @__PURE__ */ new Map();
6607
- subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6608
- const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6609
- key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6610
- formId: rec.formId,
6611
- outcome: rec.skipped ? "skipped" : "submitted",
6612
- createdAt: parseWireDate(rec.createdAt),
6613
- values: rec.values
6614
- }));
6615
6711
  setFormMarkers(markers);
6616
6712
  const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6617
6713
  (n) => n !== void 0
@@ -6649,7 +6745,18 @@ function App({ options, hostElement, bus }) {
6649
6745
  setConversationReady(true);
6650
6746
  }
6651
6747
  },
6652
- [transport, visitorId, persistence, options, bus, messagesSig, conversationIdSig, feedback, playWelcome]
6748
+ [
6749
+ transport,
6750
+ visitorId,
6751
+ persistence,
6752
+ options,
6753
+ bus,
6754
+ messagesSig,
6755
+ conversationIdSig,
6756
+ feedback,
6757
+ playWelcome,
6758
+ fetchFormMarkers
6759
+ ]
6653
6760
  );
6654
6761
  const userSig = JSON.stringify(options.userContext ?? null);
6655
6762
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6704,6 +6811,16 @@ function App({ options, hostElement, bus }) {
6704
6811
  );
6705
6812
  useEffect17(() => {
6706
6813
  void runStartConversation({ newConversation: false });
6814
+ void (async () => {
6815
+ try {
6816
+ const res = await transport.listForms();
6817
+ setRemoteForms(resolveForms(res.forms));
6818
+ } catch (err) {
6819
+ log16.debug("listForms failed \u2014 treating as no forms", { err });
6820
+ } finally {
6821
+ setFormsReady(true);
6822
+ }
6823
+ })();
6707
6824
  const persistedConversation = conversationIdSig.value;
6708
6825
  let resume = null;
6709
6826
  if (persistedConversation) {
@@ -6840,8 +6957,9 @@ function App({ options, hostElement, bus }) {
6840
6957
  [options.features.humanInLoop, handleToolResult, handleToolDecision]
6841
6958
  );
6842
6959
  const userMessageCount = () => messagesSig.value.reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0);
6960
+ const effectiveForms = options.forms.list.length > 0 ? options.forms : remoteForms ?? options.forms;
6843
6961
  const forms = useForms({
6844
- forms: options.forms,
6962
+ forms: effectiveForms,
6845
6963
  persistence,
6846
6964
  userContext: () => options.userContext,
6847
6965
  messageCount: userMessageCount,
@@ -6874,18 +6992,18 @@ function App({ options, hostElement, bus }) {
6874
6992
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
6875
6993
  const msgCount = useComputed7(() => messagesSig.value.length);
6876
6994
  useEffect17(() => {
6877
- if (conversationReady) forms.fire("pre-chat");
6878
- }, [conversationReady, forms]);
6995
+ if (conversationReady && formsReady) forms.fire("pre-chat");
6996
+ }, [conversationReady, formsReady, forms]);
6879
6997
  useEffect17(() => {
6880
- if (!canSend) forms.fire("conversation-closed");
6881
- }, [canSend, forms]);
6998
+ if (formsReady && !canSend) forms.fire("conversation-closed");
6999
+ }, [formsReady, canSend, forms]);
6882
7000
  useEffect17(() => {
6883
- if (conversationReady && pageArea) forms.fire("page-area");
6884
- }, [conversationReady, pageArea, forms]);
7001
+ if (conversationReady && formsReady && pageArea) forms.fire("page-area");
7002
+ }, [conversationReady, formsReady, pageArea, forms]);
6885
7003
  const idleMs = useMemo3(() => {
6886
- const secs = options.forms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
7004
+ const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
6887
7005
  return secs.length ? Math.min(...secs) * 1e3 : 0;
6888
- }, [options.forms]);
7006
+ }, [effectiveForms]);
6889
7007
  useEffect17(() => {
6890
7008
  if (!idleMs || !isOpen) return;
6891
7009
  const id = setTimeout(() => forms.fire("idle"), idleMs);
@@ -7062,9 +7180,13 @@ function App({ options, hostElement, bus }) {
7062
7180
  log16.info("selectConversation", { conversationId: targetConversationId });
7063
7181
  bus.emit("selectConversation", { conversationId: targetConversationId });
7064
7182
  try {
7065
- const res = await transport.loadConversation(targetConversationId);
7183
+ const [res, markers] = await Promise.all([
7184
+ transport.loadConversation(targetConversationId),
7185
+ fetchFormMarkers(targetConversationId)
7186
+ ]);
7066
7187
  const hydrated = res.messages.map(fromWireMessage);
7067
7188
  messagesSig.value = hydrated;
7189
+ setFormMarkers(markers);
7068
7190
  conversationIdSig.value = res.conversationId;
7069
7191
  persistence.saveConversationId(res.conversationId);
7070
7192
  if (res.agent) setAgent(res.agent);
@@ -7078,7 +7200,7 @@ function App({ options, hostElement, bus }) {
7078
7200
  options.onError?.(err);
7079
7201
  }
7080
7202
  },
7081
- [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
7203
+ [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
7082
7204
  );
7083
7205
  useEffect17(() => {
7084
7206
  const unsub = bindHostCommands(hostElement, {
@@ -7097,10 +7219,10 @@ function App({ options, hostElement, bus }) {
7097
7219
  );
7098
7220
  const enrichedFormMarkers = useMemo3(
7099
7221
  () => formMarkers.map((m) => {
7100
- const def = options.forms.list.find((f) => f.id === m.formId);
7222
+ const def = effectiveForms.list.find((f) => f.id === m.formId);
7101
7223
  return { ...m, title: def?.title, form: def };
7102
7224
  }),
7103
- [formMarkers, options.forms]
7225
+ [formMarkers, effectiveForms]
7104
7226
  );
7105
7227
  const panelProps = {
7106
7228
  options: effectiveOptions,