@helpai/elements 0.25.0 → 0.26.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
@@ -465,8 +465,8 @@ var DEFAULT_TRACKING = {
465
465
  sampleRate: 1
466
466
  };
467
467
  var DEFAULT_ENDPOINTS = {
468
- upload: "/ai/agent/upload-file",
469
- transcribe: "/ai/agent/transcribe-audio",
468
+ upload: "/ai-agent/upload-file",
469
+ transcribe: "/ai-agent/transcribe-audio",
470
470
  submitForm: "/submit-form"
471
471
  };
472
472
  var DEFAULT_LAUNCHER = {
@@ -510,12 +510,14 @@ function resolveOptions(rawOpts) {
510
510
  const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
511
511
  const availableLocales = i18n.availableLocales ?? [locale];
512
512
  const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
513
+ const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
513
514
  const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
514
515
  return {
515
516
  widgetId: opts.widgetId ?? "default",
516
517
  publicKey: opts.publicKey,
517
518
  aiAgentDeploymentId: opts.aiAgentDeploymentId,
518
519
  baseUrl,
520
+ agentBaseUrl,
519
521
  dataBaseUrl,
520
522
  userContext: sanitizeUserContext(opts.userContext),
521
523
  pageContext: sanitizePageContext(opts.pageContext),
@@ -829,6 +831,7 @@ var EMBED_SCALAR_ATTRS = [
829
831
  ["public-key", "publicKey"],
830
832
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
831
833
  ["base-url", "baseUrl"],
834
+ ["agent-base-url", "agentBaseUrl"],
832
835
  ["data-base-url", "dataBaseUrl"],
833
836
  ["theme", "theme", (v) => v]
834
837
  ];
@@ -977,7 +980,7 @@ function parseLauncher(get, str2) {
977
980
  return Object.keys(base).length > 0 ? base : null;
978
981
  }
979
982
 
980
- // src/core/start-conversation-shape.ts
983
+ // src/core/handshake-shape.ts
981
984
  function isPlainObject(raw) {
982
985
  return !!raw && typeof raw === "object" && !Array.isArray(raw);
983
986
  }
@@ -1305,43 +1308,43 @@ function parseWireDate(value) {
1305
1308
  }
1306
1309
  var DEFAULT_PATHS = {
1307
1310
  /** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
1308
- startConversation: "/ai/agent/start-conversation",
1311
+ handshake: "/ai-agent/handshake",
1309
1312
  /** Send a message + stream the reply (SSE). `POST`. */
1310
- streamMessage: "/ai/agent/stream-message",
1313
+ streamMessage: "/ai-agent/stream-message",
1311
1314
  /**
1312
1315
  * Resume an interrupted reply. `GET` (envelope rides as query params).
1313
1316
  * Stateless: the server re-streams the whole reply SSE again from the start —
1314
1317
  * there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
1315
1318
  * replayed events are skipped and only the continuation surfaces.
1316
1319
  */
1317
- streamResume: "/ai/agent/stream-resume",
1320
+ streamResume: "/ai-agent/stream-resume",
1318
1321
  /** Abort the in-flight reply stream. `POST`. */
1319
- cancelStream: "/ai/agent/cancel-stream",
1322
+ cancelStream: "/ai-agent/cancel-stream",
1320
1323
  /** List the visitor's conversations. `GET`. */
1321
- listConversations: "/ai/agent/list-conversations",
1322
- /** Load one conversation's messages. `GET ?conversationId=…`. */
1323
- listMessages: "/ai/agent/list-messages",
1324
+ listConversations: "/ai-agent/list-conversations",
1325
+ /** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
1326
+ listMessages: "/ai-agent/list-messages",
1324
1327
  /**
1325
1328
  * Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
1326
1329
  * server persists, returns the merged authoritative copy. Fire-and-forget on
1327
1330
  * the client — a failure leaves the local cache valid; the next
1328
- * `startConversation()` reconciles.
1331
+ * `handshake()` reconciles.
1329
1332
  */
1330
- updateSettings: "/ai/agent/update-settings",
1333
+ updateSettings: "/ai-agent/update-settings",
1331
1334
  /**
1332
1335
  * Mark a conversation read. POST `{ visitorId, conversationId }` → the server
1333
1336
  * records `lastReadAt = now` for that (visitor, conversation) and recomputes
1334
1337
  * `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
1335
1338
  * on the client; a failure just leaves the badge until the next sync.
1336
1339
  */
1337
- markRead: "/ai/agent/mark-read",
1340
+ markRead: "/ai-agent/mark-read",
1338
1341
  // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1339
1342
  /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1340
1343
  content: "/content",
1341
1344
  /**
1342
1345
  * Resolved form definitions for the deployment. `GET /forms` →
1343
1346
  * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1344
- * start-conversation); form gating waits for it. A failure is treated as
1347
+ * handshake); form gating waits for it. A failure is treated as
1345
1348
  * "no forms" (non-fatal).
1346
1349
  */
1347
1350
  forms: "/forms",
@@ -1356,7 +1359,7 @@ var DEFAULT_PATHS = {
1356
1359
  * Visitor interaction records for a conversation. `GET
1357
1360
  * /activity?visitorId=…&conversationId=…` →
1358
1361
  * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1359
- * the former `formSubmissions` echo on start-conversation / list-messages;
1362
+ * the former `formSubmissions` echo on the handshake / list-messages;
1360
1363
  * deliberately generic — future visitor interaction records ride the same
1361
1364
  * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1362
1365
  * deployment doesn't resolve) is treated as "no records" (non-fatal).
@@ -1462,7 +1465,7 @@ function sleep(ms, signal7) {
1462
1465
  var AgentTransport = class {
1463
1466
  constructor(opts) {
1464
1467
  __publicField(this, "opts", opts);
1465
- // Identity adopted from the start-conversation response; rides the request envelope
1468
+ // Identity adopted from the handshake response; rides the request envelope
1466
1469
  // (see `envelope()`) on every subsequent call so the backend can validate the
1467
1470
  // (visitor, conversation) pair.
1468
1471
  __publicField(this, "visitorId");
@@ -1485,9 +1488,9 @@ var AgentTransport = class {
1485
1488
  }
1486
1489
  /**
1487
1490
  * Seed the visitor + conversation identity from persistence BEFORE the first
1488
- * `startConversation` resolves. A mount-time `resumeStream()` runs in parallel
1489
- * with `startConversation`, so without this its envelope would carry no
1490
- * `conversationId` and the resume GET couldn't identify the thread. `startConversation`
1491
+ * `handshake` resolves. A mount-time `resumeStream()` runs in parallel
1492
+ * with `handshake`, so without this its envelope would carry no
1493
+ * `conversationId` and the resume GET couldn't identify the thread. `handshake`
1491
1494
  * later overwrites these with the server's authoritative ids (rebind-safe).
1492
1495
  */
1493
1496
  primeIdentity(visitorId, conversationId) {
@@ -1504,7 +1507,7 @@ var AgentTransport = class {
1504
1507
  return out;
1505
1508
  }
1506
1509
  /** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
1507
- * start-conversation's client-proposed ids, or a mark-read target conversation that
1510
+ * handshake's client-proposed ids, or a mark-read target conversation that
1508
1511
  * differs from the active one. */
1509
1512
  withEnvelope(body) {
1510
1513
  return { ...this.envelope(), ...body };
@@ -1525,28 +1528,22 @@ var AgentTransport = class {
1525
1528
  return url.toString();
1526
1529
  }
1527
1530
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1528
- async startConversation(body) {
1529
- log4.debug("startConversation \u2192", {
1531
+ async handshake(body) {
1532
+ log4.debug("handshake \u2192", {
1530
1533
  visitorId: body.visitorId,
1531
1534
  conversationId: body.conversationId,
1532
1535
  locale: body.locale
1533
1536
  });
1534
- const res = await this.postJson(
1535
- DEFAULT_PATHS.startConversation,
1536
- body,
1537
- "startConversation"
1538
- );
1539
- log4.debug("startConversation \u2190", {
1537
+ const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1538
+ log4.debug("handshake \u2190", {
1540
1539
  visitorId: res.visitorId,
1541
1540
  conversationId: res.conversationId,
1542
- canContinue: res.canContinue,
1543
- hasMessages: !!res.messages?.length,
1544
1541
  hasWelcome: !!res.welcome,
1545
1542
  hasConfig: !!res.config,
1546
1543
  rebind: res.rebind
1547
1544
  });
1548
- if (!isStartConversationResponseShape(res)) {
1549
- log4.warn("startConversation response did not match expected shape; using raw response", { response: res });
1545
+ if (!isHandshakeResponseShape(res)) {
1546
+ log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1550
1547
  }
1551
1548
  this.visitorId = res.visitorId ?? body.visitorId;
1552
1549
  this.conversationId = res.conversationId;
@@ -1658,7 +1655,7 @@ var AgentTransport = class {
1658
1655
  /**
1659
1656
  * Fetch the deployment's resolved form definitions. `GET /forms`
1660
1657
  * (data-API). Replaces the former `config.forms` push on the handshake —
1661
- * called once at boot, in parallel with `startConversation`; form gating
1658
+ * called once at boot, in parallel with `handshake`; form gating
1662
1659
  * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1663
1660
  * "no forms" (non-fatal).
1664
1661
  */
@@ -1700,10 +1697,10 @@ var AgentTransport = class {
1700
1697
  return res;
1701
1698
  }
1702
1699
  get uploadPath() {
1703
- return this.opts.endpoints?.upload ?? "/ai/agent/upload-file";
1700
+ return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
1704
1701
  }
1705
1702
  get transcribePath() {
1706
- return this.opts.endpoints?.transcribe ?? "/ai/agent/transcribe-audio";
1703
+ return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
1707
1704
  }
1708
1705
  get submitFormPath() {
1709
1706
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
@@ -1743,9 +1740,9 @@ var AgentTransport = class {
1743
1740
  * Re-attach to an in-flight reply on a cold page load / refresh.
1744
1741
  *
1745
1742
  * UNCONDITIONAL by design — the caller runs this in parallel with
1746
- * `startConversation`, never gated on whether there are persisted messages. If
1743
+ * `handshake`, never gated on whether there are persisted messages. If
1747
1744
  * the user refreshed while the assistant was still streaming, the
1748
- * `startConversation` reply carries no messages (the turn isn't persisted until
1745
+ * `handshake` reply carries no messages (the turn isn't persisted until
1749
1746
  * it finishes) and THIS is the channel that resumes the live reply. When
1750
1747
  * nothing is in flight the server answers 204 and the handle yields nothing
1751
1748
  * (the common case — no bubble is created).
@@ -1772,7 +1769,7 @@ var AgentTransport = class {
1772
1769
  * - after a POST /stream-message drops mid-reply (`immediate=false` — back
1773
1770
  * off first, the reply may still be spinning up server-side);
1774
1771
  * - on a cold page load / refresh (`immediate=true` — hit it at once, in
1775
- * parallel with start-conversation, so a reply that was streaming when the
1772
+ * parallel with handshake, so a reply that was streaming when the
1776
1773
  * user reloaded keeps flowing).
1777
1774
  *
1778
1775
  * The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
@@ -1781,7 +1778,7 @@ var AgentTransport = class {
1781
1778
  * nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
1782
1779
  * signalled by a segment that ends without a terminal chunk AND surfaced no
1783
1780
  * NEW events — not by a status code. We stop gracefully there; the completed
1784
- * turn comes from the DB via start-conversation / list-messages.
1781
+ * turn comes from the DB via the handshake / list-messages.
1785
1782
  *
1786
1783
  * Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
1787
1784
  * a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
@@ -1846,7 +1843,7 @@ var AgentTransport = class {
1846
1843
  }
1847
1844
  return false;
1848
1845
  }
1849
- /** Abort + fire-and-forget POST to `/ai/agent/cancel-stream`. Idempotent. */
1846
+ /** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
1850
1847
  cancelStream(ctrl, conversationId) {
1851
1848
  if (ctrl.signal.aborted) return;
1852
1849
  log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
@@ -1887,7 +1884,7 @@ var AgentTransport = class {
1887
1884
  return url.toString();
1888
1885
  }
1889
1886
  // JSON requests are idempotent (reads, or full-snapshot writes like
1890
- // update-settings / mark-read / start-conversation), so they auto-retry transient
1887
+ // update-settings / mark-read / handshake), so they auto-retry transient
1891
1888
  // failures. The message POST is NOT in here — re-sending could duplicate the
1892
1889
  // reply, so it surfaces an error for the user to retry (see `openMessageStream`).
1893
1890
  async postJson(path, body, label) {
@@ -1956,23 +1953,22 @@ var AgentTransport = class {
1956
1953
  return await res.json();
1957
1954
  }
1958
1955
  /**
1959
- * Agent-API URL — resolves a `/ai/agent/*` path against the agent module
1960
- * base, `${baseUrl}/api/agent`. Absolute URLs pass through unchanged.
1956
+ * Agent-API URL — resolves a `/ai-agent/*` path against the resolved
1957
+ * agent module base. Absolute URLs pass through unchanged.
1961
1958
  */
1962
1959
  url(pathOrUrl) {
1963
1960
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1964
- return `${this.opts.baseUrl}/api/agent${pathOrUrl}`;
1961
+ return `${this.opts.agentBaseUrl}${pathOrUrl}`;
1965
1962
  }
1966
1963
  /**
1967
1964
  * 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`.
1965
+ * forms / submit-form / activity) against the resolved data module base.
1966
+ * Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
1967
+ * CRM) pass through unchanged.
1972
1968
  */
1973
1969
  dataUrl(pathOrUrl) {
1974
1970
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1975
- return `${this.opts.dataBaseUrl ?? `${this.opts.baseUrl}/api/data`}${pathOrUrl}`;
1971
+ return `${this.opts.dataBaseUrl}${pathOrUrl}`;
1976
1972
  }
1977
1973
  get fetchImpl() {
1978
1974
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -1986,10 +1982,10 @@ function extensionFor(mimeType) {
1986
1982
  if (mimeType.includes("wav")) return "wav";
1987
1983
  return "bin";
1988
1984
  }
1989
- function isStartConversationResponseShape(raw) {
1985
+ function isHandshakeResponseShape(raw) {
1990
1986
  if (!raw || typeof raw !== "object") return false;
1991
1987
  const r = raw;
1992
- return typeof r.visitorId === "string" && typeof r.conversationId === "string" && typeof r.canContinue === "boolean";
1988
+ return typeof r.visitorId === "string" && typeof r.conversationId === "string";
1993
1989
  }
1994
1990
 
1995
1991
  // src/stream/messages.ts
@@ -6504,6 +6500,14 @@ function App({ options, hostElement, bus }) {
6504
6500
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
6505
6501
  );
6506
6502
  const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
6503
+ const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
6504
+ const activatedRef = useRef9(activated);
6505
+ activatedRef.current = activated;
6506
+ const pendingThreadRef = useRef9(false);
6507
+ const welcomeRef = useRef9(void 0);
6508
+ useEffect17(() => {
6509
+ if (isOpen) setActivated(true);
6510
+ }, [isOpen]);
6507
6511
  const initialPanelApplied = useRef9(false);
6508
6512
  const [launcherLeaving, setLauncherLeaving] = useState13(false);
6509
6513
  const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
@@ -6530,7 +6534,7 @@ function App({ options, hostElement, bus }) {
6530
6534
  const [formContext, setFormContext] = useState13({});
6531
6535
  const [transport] = useState13(
6532
6536
  () => new AgentTransport({
6533
- baseUrl: options.baseUrl,
6537
+ agentBaseUrl: options.agentBaseUrl,
6534
6538
  dataBaseUrl: options.dataBaseUrl,
6535
6539
  auth: createAuth({
6536
6540
  publicKey: options.publicKey,
@@ -6633,7 +6637,46 @@ function App({ options, hostElement, bus }) {
6633
6637
  [transport]
6634
6638
  );
6635
6639
  const connectGenRef = useRef9(0);
6636
- const runStartConversation = useCallback6(
6640
+ const loadThread = useCallback6(
6641
+ async (conversationId) => {
6642
+ const myGen = connectGenRef.current;
6643
+ const isStale = () => myGen !== connectGenRef.current;
6644
+ setLoadingMessages(true);
6645
+ try {
6646
+ const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
6647
+ if (isStale()) return;
6648
+ const loaded = (chat.messages ?? []).map(fromWireMessage);
6649
+ setFormMarkers(markers);
6650
+ const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6651
+ (n) => n !== void 0
6652
+ );
6653
+ const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
6654
+ const loadedIds = new Set(loaded.map((m) => m.id));
6655
+ const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
6656
+ const row = makeInstantWelcomeMessage(w);
6657
+ row.createdAt = welcomeAnchor;
6658
+ return row;
6659
+ });
6660
+ const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
6661
+ if (loaded.length || tail.length) {
6662
+ messagesSig.value = [...welcomeRows, ...loaded, ...tail];
6663
+ setCanSend(chat.canContinue ?? true);
6664
+ setSuggestions(chat.suggestions ?? []);
6665
+ } else {
6666
+ messagesSig.value = welcomeRows;
6667
+ }
6668
+ } catch (err) {
6669
+ if (isStale()) return;
6670
+ log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
6671
+ conversationIdSig.value = void 0;
6672
+ persistence.saveConversationId(void 0);
6673
+ } finally {
6674
+ if (!isStale()) setLoadingMessages(false);
6675
+ }
6676
+ },
6677
+ [transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
6678
+ );
6679
+ const runHandshake = useCallback6(
6637
6680
  async ({ newConversation }) => {
6638
6681
  const myGen = ++connectGenRef.current;
6639
6682
  const isStale = () => myGen !== connectGenRef.current;
@@ -6645,7 +6688,7 @@ function App({ options, hostElement, bus }) {
6645
6688
  }
6646
6689
  let res;
6647
6690
  try {
6648
- res = await transport.startConversation({
6691
+ res = await transport.handshake({
6649
6692
  visitorId,
6650
6693
  conversationId,
6651
6694
  userPrefs: persistence.loadUserPrefs(),
@@ -6696,67 +6739,22 @@ function App({ options, hostElement, bus }) {
6696
6739
  if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
6697
6740
  if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
6698
6741
  }
6742
+ welcomeRef.current = res.welcome;
6699
6743
  const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
6700
6744
  if (isResume) {
6701
- setLoadingMessages(true);
6702
- try {
6703
- const chatPromise = res.messages?.length ? Promise.resolve({
6704
- conversationId: persistedChatId,
6705
- canContinue: res.canContinue ?? true,
6706
- messages: res.messages
6707
- }) : transport.loadConversation(persistedChatId);
6708
- const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6709
- if (isStale()) return;
6710
- const loaded = (chat.messages ?? []).map(fromWireMessage);
6711
- setFormMarkers(markers);
6712
- const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6713
- (n) => n !== void 0
6714
- );
6715
- const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
6716
- const loadedIds = new Set(loaded.map((m) => m.id));
6717
- const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
6718
- const row = makeInstantWelcomeMessage(w);
6719
- row.createdAt = welcomeAnchor;
6720
- return row;
6721
- });
6722
- const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
6723
- if (loaded.length || tail.length) {
6724
- messagesSig.value = [...welcomeRows, ...loaded, ...tail];
6725
- setCanSend(chat.canContinue ?? true);
6726
- setSuggestions(chat.suggestions ?? []);
6727
- } else {
6728
- messagesSig.value = welcomeRows;
6729
- }
6730
- } catch (err) {
6731
- if (isStale()) return;
6732
- log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
6733
- conversationIdSig.value = void 0;
6734
- persistence.saveConversationId(void 0);
6735
- } finally {
6736
- if (!isStale()) setLoadingMessages(false);
6737
- }
6745
+ if (activatedRef.current) void loadThread(persistedChatId);
6746
+ else pendingThreadRef.current = true;
6738
6747
  } else {
6739
6748
  conversationIdSig.value = res.conversationId;
6740
6749
  persistence.saveConversationId(res.conversationId);
6741
6750
  playWelcome(res.welcome?.messages);
6742
6751
  }
6743
6752
  if (!newConversation) {
6744
- bus.emit("conversationStarted", res);
6753
+ bus.emit("handshake", res);
6745
6754
  setConversationReady(true);
6746
6755
  }
6747
6756
  },
6748
- [
6749
- transport,
6750
- visitorId,
6751
- persistence,
6752
- options,
6753
- bus,
6754
- messagesSig,
6755
- conversationIdSig,
6756
- feedback,
6757
- playWelcome,
6758
- fetchFormMarkers
6759
- ]
6757
+ [transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
6760
6758
  );
6761
6759
  const userSig = JSON.stringify(options.userContext ?? null);
6762
6760
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6809,8 +6807,18 @@ function App({ options, hostElement, bus }) {
6809
6807
  },
6810
6808
  [reducer, messagesSig, feedback, bus, options, homeNav]
6811
6809
  );
6810
+ const resumeHandleRef = useRef9(null);
6812
6811
  useEffect17(() => {
6813
- void runStartConversation({ newConversation: false });
6812
+ const persistedConversation = conversationIdSig.value;
6813
+ if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
6814
+ void runHandshake({ newConversation: false });
6815
+ return () => {
6816
+ connectGenRef.current++;
6817
+ resumeHandleRef.current?.cancel();
6818
+ };
6819
+ }, []);
6820
+ useEffect17(() => {
6821
+ if (!activated) return;
6814
6822
  void (async () => {
6815
6823
  try {
6816
6824
  const res = await transport.listForms();
@@ -6821,24 +6829,23 @@ function App({ options, hostElement, bus }) {
6821
6829
  setFormsReady(true);
6822
6830
  }
6823
6831
  })();
6824
- const persistedConversation = conversationIdSig.value;
6825
- let resume = null;
6826
- if (persistedConversation) {
6827
- transport.primeIdentity(visitorId, persistedConversation);
6828
- resume = transport.resumeStream();
6829
- void runResume(resume);
6832
+ if (pendingThreadRef.current) {
6833
+ pendingThreadRef.current = false;
6834
+ const persisted = conversationIdSig.value;
6835
+ if (persisted) void loadThread(persisted);
6830
6836
  }
6831
- return () => {
6832
- connectGenRef.current++;
6833
- resume?.cancel();
6834
- };
6835
- }, []);
6837
+ if (conversationIdSig.value) {
6838
+ const handle = transport.resumeStream();
6839
+ resumeHandleRef.current = handle;
6840
+ void runResume(handle);
6841
+ }
6842
+ }, [activated]);
6836
6843
  const lastUserSig = useRef9(userSig);
6837
6844
  useEffect17(() => {
6838
6845
  if (!conversationReady || lastUserSig.current === userSig) return;
6839
6846
  lastUserSig.current = userSig;
6840
- void runStartConversation({ newConversation: false });
6841
- }, [userSig, conversationReady, runStartConversation]);
6847
+ void runHandshake({ newConversation: false });
6848
+ }, [userSig, conversationReady, runHandshake]);
6842
6849
  useEffect17(() => {
6843
6850
  applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
6844
6851
  }, [hostElement, isOpen, panelSize, options.mode]);
@@ -7065,10 +7072,10 @@ function App({ options, hostElement, bus }) {
7065
7072
  }, [transport, options.modules, unreadCountSig]);
7066
7073
  const unreadSeeded = useRef9(false);
7067
7074
  useEffect17(() => {
7068
- if (!conversationReady || unreadSeeded.current) return;
7075
+ if (!activated || !conversationReady || unreadSeeded.current) return;
7069
7076
  unreadSeeded.current = true;
7070
7077
  refreshUnread();
7071
- }, [conversationReady, refreshUnread]);
7078
+ }, [activated, conversationReady, refreshUnread]);
7072
7079
  const handleOpen = useCallback6(() => {
7073
7080
  log16.info("open", { mode: options.mode });
7074
7081
  feedback.unlock();
@@ -7090,8 +7097,8 @@ function App({ options, hostElement, bus }) {
7090
7097
  persistence.clearConversation();
7091
7098
  setCanSend(true);
7092
7099
  setFormMarkers([]);
7093
- void runStartConversation({ newConversation: true });
7094
- }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runStartConversation, bus]);
7100
+ void runHandshake({ newConversation: true });
7101
+ }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
7095
7102
  const handleNewChat = useCallback6(() => {
7096
7103
  handleClear();
7097
7104
  setView("chat");
@@ -7449,7 +7456,7 @@ var ChatElement = class extends HTMLElement {
7449
7456
  /**
7450
7457
  * Raw options accumulator. Starts as whatever the page-owner set via
7451
7458
  * HTML attributes; gets server-side settings merged in on the
7452
- * `conversationStarted` event so subsequent re-renders pick up the server's
7459
+ * `handshake` event so subsequent re-renders pick up the server's
7453
7460
  * mode / theme / launcher / etc.
7454
7461
  *
7455
7462
  * Mirrors `index.ts`'s `rawOptions` so both entry points share the
@@ -7479,7 +7486,7 @@ var ChatElement = class extends HTMLElement {
7479
7486
  existingHost: this,
7480
7487
  position: hostPositionForMode(resolved.mode)
7481
7488
  });
7482
- this.bus.on("conversationStarted", (response) => {
7489
+ this.bus.on("handshake", (response) => {
7483
7490
  if (!response.config) return;
7484
7491
  this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
7485
7492
  this.renderApp();
@@ -7489,7 +7496,7 @@ var ChatElement = class extends HTMLElement {
7489
7496
  }
7490
7497
  /**
7491
7498
  * Apply the current `rawOptions` to the host element + App tree.
7492
- * Called by both the initial boot and every `conversationStarted` merge.
7499
+ * Called by both the initial boot and every `handshake` merge.
7493
7500
  */
7494
7501
  renderApp() {
7495
7502
  if (!this.mountResult) return;