@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/index.mjs CHANGED
@@ -480,8 +480,8 @@ var DEFAULT_TRACKING = {
480
480
  sampleRate: 1
481
481
  };
482
482
  var DEFAULT_ENDPOINTS = {
483
- upload: "/ai/agent/upload-file",
484
- transcribe: "/ai/agent/transcribe-audio",
483
+ upload: "/ai-agent/upload-file",
484
+ transcribe: "/ai-agent/transcribe-audio",
485
485
  submitForm: "/submit-form"
486
486
  };
487
487
  var DEFAULT_LAUNCHER = {
@@ -525,12 +525,14 @@ function resolveOptions(rawOpts) {
525
525
  const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
526
526
  const availableLocales = i18n.availableLocales ?? [locale];
527
527
  const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
528
+ const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
528
529
  const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
529
530
  return {
530
531
  widgetId: opts.widgetId ?? "default",
531
532
  publicKey: opts.publicKey,
532
533
  aiAgentDeploymentId: opts.aiAgentDeploymentId,
533
534
  baseUrl,
535
+ agentBaseUrl,
534
536
  dataBaseUrl,
535
537
  userContext: sanitizeUserContext(opts.userContext),
536
538
  pageContext: sanitizePageContext(opts.pageContext),
@@ -844,6 +846,7 @@ var EMBED_SCALAR_ATTRS = [
844
846
  ["public-key", "publicKey"],
845
847
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
846
848
  ["base-url", "baseUrl"],
849
+ ["agent-base-url", "agentBaseUrl"],
847
850
  ["data-base-url", "dataBaseUrl"],
848
851
  ["theme", "theme", (v) => v]
849
852
  ];
@@ -919,7 +922,7 @@ var REACTIVE_ATTRS = [
919
922
  ...SPECIAL_REACTIVE_ATTRS
920
923
  ];
921
924
 
922
- // src/core/start-conversation-shape.ts
925
+ // src/core/handshake-shape.ts
923
926
  function isPlainObject(raw) {
924
927
  return !!raw && typeof raw === "object" && !Array.isArray(raw);
925
928
  }
@@ -1250,43 +1253,43 @@ function parseWireDate(value) {
1250
1253
  }
1251
1254
  var DEFAULT_PATHS = {
1252
1255
  /** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
1253
- startConversation: "/ai/agent/start-conversation",
1256
+ handshake: "/ai-agent/handshake",
1254
1257
  /** Send a message + stream the reply (SSE). `POST`. */
1255
- streamMessage: "/ai/agent/stream-message",
1258
+ streamMessage: "/ai-agent/stream-message",
1256
1259
  /**
1257
1260
  * Resume an interrupted reply. `GET` (envelope rides as query params).
1258
1261
  * Stateless: the server re-streams the whole reply SSE again from the start —
1259
1262
  * there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
1260
1263
  * replayed events are skipped and only the continuation surfaces.
1261
1264
  */
1262
- streamResume: "/ai/agent/stream-resume",
1265
+ streamResume: "/ai-agent/stream-resume",
1263
1266
  /** Abort the in-flight reply stream. `POST`. */
1264
- cancelStream: "/ai/agent/cancel-stream",
1267
+ cancelStream: "/ai-agent/cancel-stream",
1265
1268
  /** List the visitor's conversations. `GET`. */
1266
- listConversations: "/ai/agent/list-conversations",
1267
- /** Load one conversation's messages. `GET ?conversationId=…`. */
1268
- listMessages: "/ai/agent/list-messages",
1269
+ listConversations: "/ai-agent/list-conversations",
1270
+ /** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
1271
+ listMessages: "/ai-agent/list-messages",
1269
1272
  /**
1270
1273
  * Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
1271
1274
  * server persists, returns the merged authoritative copy. Fire-and-forget on
1272
1275
  * the client — a failure leaves the local cache valid; the next
1273
- * `startConversation()` reconciles.
1276
+ * `handshake()` reconciles.
1274
1277
  */
1275
- updateSettings: "/ai/agent/update-settings",
1278
+ updateSettings: "/ai-agent/update-settings",
1276
1279
  /**
1277
1280
  * Mark a conversation read. POST `{ visitorId, conversationId }` → the server
1278
1281
  * records `lastReadAt = now` for that (visitor, conversation) and recomputes
1279
1282
  * `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
1280
1283
  * on the client; a failure just leaves the badge until the next sync.
1281
1284
  */
1282
- markRead: "/ai/agent/mark-read",
1285
+ markRead: "/ai-agent/mark-read",
1283
1286
  // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1284
1287
  /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1285
1288
  content: "/content",
1286
1289
  /**
1287
1290
  * Resolved form definitions for the deployment. `GET /forms` →
1288
1291
  * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1289
- * start-conversation); form gating waits for it. A failure is treated as
1292
+ * handshake); form gating waits for it. A failure is treated as
1290
1293
  * "no forms" (non-fatal).
1291
1294
  */
1292
1295
  forms: "/forms",
@@ -1301,7 +1304,7 @@ var DEFAULT_PATHS = {
1301
1304
  * Visitor interaction records for a conversation. `GET
1302
1305
  * /activity?visitorId=…&conversationId=…` →
1303
1306
  * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1304
- * the former `formSubmissions` echo on start-conversation / list-messages;
1307
+ * the former `formSubmissions` echo on the handshake / list-messages;
1305
1308
  * deliberately generic — future visitor interaction records ride the same
1306
1309
  * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1307
1310
  * deployment doesn't resolve) is treated as "no records" (non-fatal).
@@ -1407,7 +1410,7 @@ function sleep(ms, signal7) {
1407
1410
  var AgentTransport = class {
1408
1411
  constructor(opts) {
1409
1412
  __publicField(this, "opts", opts);
1410
- // Identity adopted from the start-conversation response; rides the request envelope
1413
+ // Identity adopted from the handshake response; rides the request envelope
1411
1414
  // (see `envelope()`) on every subsequent call so the backend can validate the
1412
1415
  // (visitor, conversation) pair.
1413
1416
  __publicField(this, "visitorId");
@@ -1430,9 +1433,9 @@ var AgentTransport = class {
1430
1433
  }
1431
1434
  /**
1432
1435
  * Seed the visitor + conversation identity from persistence BEFORE the first
1433
- * `startConversation` resolves. A mount-time `resumeStream()` runs in parallel
1434
- * with `startConversation`, so without this its envelope would carry no
1435
- * `conversationId` and the resume GET couldn't identify the thread. `startConversation`
1436
+ * `handshake` resolves. A mount-time `resumeStream()` runs in parallel
1437
+ * with `handshake`, so without this its envelope would carry no
1438
+ * `conversationId` and the resume GET couldn't identify the thread. `handshake`
1436
1439
  * later overwrites these with the server's authoritative ids (rebind-safe).
1437
1440
  */
1438
1441
  primeIdentity(visitorId, conversationId) {
@@ -1449,7 +1452,7 @@ var AgentTransport = class {
1449
1452
  return out;
1450
1453
  }
1451
1454
  /** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
1452
- * start-conversation's client-proposed ids, or a mark-read target conversation that
1455
+ * handshake's client-proposed ids, or a mark-read target conversation that
1453
1456
  * differs from the active one. */
1454
1457
  withEnvelope(body) {
1455
1458
  return { ...this.envelope(), ...body };
@@ -1470,28 +1473,22 @@ var AgentTransport = class {
1470
1473
  return url.toString();
1471
1474
  }
1472
1475
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1473
- async startConversation(body) {
1474
- log4.debug("startConversation \u2192", {
1476
+ async handshake(body) {
1477
+ log4.debug("handshake \u2192", {
1475
1478
  visitorId: body.visitorId,
1476
1479
  conversationId: body.conversationId,
1477
1480
  locale: body.locale
1478
1481
  });
1479
- const res = await this.postJson(
1480
- DEFAULT_PATHS.startConversation,
1481
- body,
1482
- "startConversation"
1483
- );
1484
- log4.debug("startConversation \u2190", {
1482
+ const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1483
+ log4.debug("handshake \u2190", {
1485
1484
  visitorId: res.visitorId,
1486
1485
  conversationId: res.conversationId,
1487
- canContinue: res.canContinue,
1488
- hasMessages: !!res.messages?.length,
1489
1486
  hasWelcome: !!res.welcome,
1490
1487
  hasConfig: !!res.config,
1491
1488
  rebind: res.rebind
1492
1489
  });
1493
- if (!isStartConversationResponseShape(res)) {
1494
- log4.warn("startConversation response did not match expected shape; using raw response", { response: res });
1490
+ if (!isHandshakeResponseShape(res)) {
1491
+ log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1495
1492
  }
1496
1493
  this.visitorId = res.visitorId ?? body.visitorId;
1497
1494
  this.conversationId = res.conversationId;
@@ -1603,7 +1600,7 @@ var AgentTransport = class {
1603
1600
  /**
1604
1601
  * Fetch the deployment's resolved form definitions. `GET /forms`
1605
1602
  * (data-API). Replaces the former `config.forms` push on the handshake —
1606
- * called once at boot, in parallel with `startConversation`; form gating
1603
+ * called once at boot, in parallel with `handshake`; form gating
1607
1604
  * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1608
1605
  * "no forms" (non-fatal).
1609
1606
  */
@@ -1645,10 +1642,10 @@ var AgentTransport = class {
1645
1642
  return res;
1646
1643
  }
1647
1644
  get uploadPath() {
1648
- return this.opts.endpoints?.upload ?? "/ai/agent/upload-file";
1645
+ return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
1649
1646
  }
1650
1647
  get transcribePath() {
1651
- return this.opts.endpoints?.transcribe ?? "/ai/agent/transcribe-audio";
1648
+ return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
1652
1649
  }
1653
1650
  get submitFormPath() {
1654
1651
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
@@ -1688,9 +1685,9 @@ var AgentTransport = class {
1688
1685
  * Re-attach to an in-flight reply on a cold page load / refresh.
1689
1686
  *
1690
1687
  * UNCONDITIONAL by design — the caller runs this in parallel with
1691
- * `startConversation`, never gated on whether there are persisted messages. If
1688
+ * `handshake`, never gated on whether there are persisted messages. If
1692
1689
  * the user refreshed while the assistant was still streaming, the
1693
- * `startConversation` reply carries no messages (the turn isn't persisted until
1690
+ * `handshake` reply carries no messages (the turn isn't persisted until
1694
1691
  * it finishes) and THIS is the channel that resumes the live reply. When
1695
1692
  * nothing is in flight the server answers 204 and the handle yields nothing
1696
1693
  * (the common case — no bubble is created).
@@ -1717,7 +1714,7 @@ var AgentTransport = class {
1717
1714
  * - after a POST /stream-message drops mid-reply (`immediate=false` — back
1718
1715
  * off first, the reply may still be spinning up server-side);
1719
1716
  * - on a cold page load / refresh (`immediate=true` — hit it at once, in
1720
- * parallel with start-conversation, so a reply that was streaming when the
1717
+ * parallel with handshake, so a reply that was streaming when the
1721
1718
  * user reloaded keeps flowing).
1722
1719
  *
1723
1720
  * The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
@@ -1726,7 +1723,7 @@ var AgentTransport = class {
1726
1723
  * nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
1727
1724
  * signalled by a segment that ends without a terminal chunk AND surfaced no
1728
1725
  * NEW events — not by a status code. We stop gracefully there; the completed
1729
- * turn comes from the DB via start-conversation / list-messages.
1726
+ * turn comes from the DB via the handshake / list-messages.
1730
1727
  *
1731
1728
  * Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
1732
1729
  * a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
@@ -1791,7 +1788,7 @@ var AgentTransport = class {
1791
1788
  }
1792
1789
  return false;
1793
1790
  }
1794
- /** Abort + fire-and-forget POST to `/ai/agent/cancel-stream`. Idempotent. */
1791
+ /** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
1795
1792
  cancelStream(ctrl, conversationId) {
1796
1793
  if (ctrl.signal.aborted) return;
1797
1794
  log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
@@ -1832,7 +1829,7 @@ var AgentTransport = class {
1832
1829
  return url.toString();
1833
1830
  }
1834
1831
  // JSON requests are idempotent (reads, or full-snapshot writes like
1835
- // update-settings / mark-read / start-conversation), so they auto-retry transient
1832
+ // update-settings / mark-read / handshake), so they auto-retry transient
1836
1833
  // failures. The message POST is NOT in here — re-sending could duplicate the
1837
1834
  // reply, so it surfaces an error for the user to retry (see `openMessageStream`).
1838
1835
  async postJson(path, body, label) {
@@ -1901,23 +1898,22 @@ var AgentTransport = class {
1901
1898
  return await res.json();
1902
1899
  }
1903
1900
  /**
1904
- * Agent-API URL — resolves a `/ai/agent/*` path against the agent module
1905
- * base, `${baseUrl}/api/agent`. Absolute URLs pass through unchanged.
1901
+ * Agent-API URL — resolves a `/ai-agent/*` path against the resolved
1902
+ * agent module base. Absolute URLs pass through unchanged.
1906
1903
  */
1907
1904
  url(pathOrUrl) {
1908
1905
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1909
- return `${this.opts.baseUrl}/api/agent${pathOrUrl}`;
1906
+ return `${this.opts.agentBaseUrl}${pathOrUrl}`;
1910
1907
  }
1911
1908
  /**
1912
1909
  * Data-API variant of {@link url} — resolves a root-level path (content /
1913
- * forms / submit-form / activity) against `dataBaseUrl`. Absolute URLs
1914
- * (e.g. an `endpoints.submitForm` pointing straight at a CRM) pass through
1915
- * unchanged; with no `dataBaseUrl` configured the data module derives from
1916
- * the main origin: `${baseUrl}/api/data`.
1910
+ * forms / submit-form / activity) against the resolved data module base.
1911
+ * Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
1912
+ * CRM) pass through unchanged.
1917
1913
  */
1918
1914
  dataUrl(pathOrUrl) {
1919
1915
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1920
- return `${this.opts.dataBaseUrl ?? `${this.opts.baseUrl}/api/data`}${pathOrUrl}`;
1916
+ return `${this.opts.dataBaseUrl}${pathOrUrl}`;
1921
1917
  }
1922
1918
  get fetchImpl() {
1923
1919
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -1931,10 +1927,10 @@ function extensionFor(mimeType) {
1931
1927
  if (mimeType.includes("wav")) return "wav";
1932
1928
  return "bin";
1933
1929
  }
1934
- function isStartConversationResponseShape(raw) {
1930
+ function isHandshakeResponseShape(raw) {
1935
1931
  if (!raw || typeof raw !== "object") return false;
1936
1932
  const r = raw;
1937
- return typeof r.visitorId === "string" && typeof r.conversationId === "string" && typeof r.canContinue === "boolean";
1933
+ return typeof r.visitorId === "string" && typeof r.conversationId === "string";
1938
1934
  }
1939
1935
 
1940
1936
  // src/stream/messages.ts
@@ -6449,6 +6445,14 @@ function App({ options, hostElement, bus }) {
6449
6445
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
6450
6446
  );
6451
6447
  const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
6448
+ const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
6449
+ const activatedRef = useRef9(activated);
6450
+ activatedRef.current = activated;
6451
+ const pendingThreadRef = useRef9(false);
6452
+ const welcomeRef = useRef9(void 0);
6453
+ useEffect17(() => {
6454
+ if (isOpen) setActivated(true);
6455
+ }, [isOpen]);
6452
6456
  const initialPanelApplied = useRef9(false);
6453
6457
  const [launcherLeaving, setLauncherLeaving] = useState13(false);
6454
6458
  const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
@@ -6475,7 +6479,7 @@ function App({ options, hostElement, bus }) {
6475
6479
  const [formContext, setFormContext] = useState13({});
6476
6480
  const [transport] = useState13(
6477
6481
  () => new AgentTransport({
6478
- baseUrl: options.baseUrl,
6482
+ agentBaseUrl: options.agentBaseUrl,
6479
6483
  dataBaseUrl: options.dataBaseUrl,
6480
6484
  auth: createAuth({
6481
6485
  publicKey: options.publicKey,
@@ -6578,7 +6582,46 @@ function App({ options, hostElement, bus }) {
6578
6582
  [transport]
6579
6583
  );
6580
6584
  const connectGenRef = useRef9(0);
6581
- const runStartConversation = useCallback6(
6585
+ const loadThread = useCallback6(
6586
+ async (conversationId) => {
6587
+ const myGen = connectGenRef.current;
6588
+ const isStale = () => myGen !== connectGenRef.current;
6589
+ setLoadingMessages(true);
6590
+ try {
6591
+ const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
6592
+ if (isStale()) return;
6593
+ const loaded = (chat.messages ?? []).map(fromWireMessage);
6594
+ setFormMarkers(markers);
6595
+ const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6596
+ (n) => n !== void 0
6597
+ );
6598
+ const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
6599
+ const loadedIds = new Set(loaded.map((m) => m.id));
6600
+ const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
6601
+ const row = makeInstantWelcomeMessage(w);
6602
+ row.createdAt = welcomeAnchor;
6603
+ return row;
6604
+ });
6605
+ const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
6606
+ if (loaded.length || tail.length) {
6607
+ messagesSig.value = [...welcomeRows, ...loaded, ...tail];
6608
+ setCanSend(chat.canContinue ?? true);
6609
+ setSuggestions(chat.suggestions ?? []);
6610
+ } else {
6611
+ messagesSig.value = welcomeRows;
6612
+ }
6613
+ } catch (err) {
6614
+ if (isStale()) return;
6615
+ log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
6616
+ conversationIdSig.value = void 0;
6617
+ persistence.saveConversationId(void 0);
6618
+ } finally {
6619
+ if (!isStale()) setLoadingMessages(false);
6620
+ }
6621
+ },
6622
+ [transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
6623
+ );
6624
+ const runHandshake = useCallback6(
6582
6625
  async ({ newConversation }) => {
6583
6626
  const myGen = ++connectGenRef.current;
6584
6627
  const isStale = () => myGen !== connectGenRef.current;
@@ -6590,7 +6633,7 @@ function App({ options, hostElement, bus }) {
6590
6633
  }
6591
6634
  let res;
6592
6635
  try {
6593
- res = await transport.startConversation({
6636
+ res = await transport.handshake({
6594
6637
  visitorId,
6595
6638
  conversationId,
6596
6639
  userPrefs: persistence.loadUserPrefs(),
@@ -6641,67 +6684,22 @@ function App({ options, hostElement, bus }) {
6641
6684
  if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
6642
6685
  if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
6643
6686
  }
6687
+ welcomeRef.current = res.welcome;
6644
6688
  const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
6645
6689
  if (isResume) {
6646
- setLoadingMessages(true);
6647
- try {
6648
- const chatPromise = res.messages?.length ? Promise.resolve({
6649
- conversationId: persistedChatId,
6650
- canContinue: res.canContinue ?? true,
6651
- messages: res.messages
6652
- }) : transport.loadConversation(persistedChatId);
6653
- const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6654
- if (isStale()) return;
6655
- const loaded = (chat.messages ?? []).map(fromWireMessage);
6656
- setFormMarkers(markers);
6657
- const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6658
- (n) => n !== void 0
6659
- );
6660
- const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
6661
- const loadedIds = new Set(loaded.map((m) => m.id));
6662
- const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
6663
- const row = makeInstantWelcomeMessage(w);
6664
- row.createdAt = welcomeAnchor;
6665
- return row;
6666
- });
6667
- const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
6668
- if (loaded.length || tail.length) {
6669
- messagesSig.value = [...welcomeRows, ...loaded, ...tail];
6670
- setCanSend(chat.canContinue ?? true);
6671
- setSuggestions(chat.suggestions ?? []);
6672
- } else {
6673
- messagesSig.value = welcomeRows;
6674
- }
6675
- } catch (err) {
6676
- if (isStale()) return;
6677
- log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
6678
- conversationIdSig.value = void 0;
6679
- persistence.saveConversationId(void 0);
6680
- } finally {
6681
- if (!isStale()) setLoadingMessages(false);
6682
- }
6690
+ if (activatedRef.current) void loadThread(persistedChatId);
6691
+ else pendingThreadRef.current = true;
6683
6692
  } else {
6684
6693
  conversationIdSig.value = res.conversationId;
6685
6694
  persistence.saveConversationId(res.conversationId);
6686
6695
  playWelcome(res.welcome?.messages);
6687
6696
  }
6688
6697
  if (!newConversation) {
6689
- bus.emit("conversationStarted", res);
6698
+ bus.emit("handshake", res);
6690
6699
  setConversationReady(true);
6691
6700
  }
6692
6701
  },
6693
- [
6694
- transport,
6695
- visitorId,
6696
- persistence,
6697
- options,
6698
- bus,
6699
- messagesSig,
6700
- conversationIdSig,
6701
- feedback,
6702
- playWelcome,
6703
- fetchFormMarkers
6704
- ]
6702
+ [transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
6705
6703
  );
6706
6704
  const userSig = JSON.stringify(options.userContext ?? null);
6707
6705
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6754,8 +6752,18 @@ function App({ options, hostElement, bus }) {
6754
6752
  },
6755
6753
  [reducer, messagesSig, feedback, bus, options, homeNav]
6756
6754
  );
6755
+ const resumeHandleRef = useRef9(null);
6757
6756
  useEffect17(() => {
6758
- void runStartConversation({ newConversation: false });
6757
+ const persistedConversation = conversationIdSig.value;
6758
+ if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
6759
+ void runHandshake({ newConversation: false });
6760
+ return () => {
6761
+ connectGenRef.current++;
6762
+ resumeHandleRef.current?.cancel();
6763
+ };
6764
+ }, []);
6765
+ useEffect17(() => {
6766
+ if (!activated) return;
6759
6767
  void (async () => {
6760
6768
  try {
6761
6769
  const res = await transport.listForms();
@@ -6766,24 +6774,23 @@ function App({ options, hostElement, bus }) {
6766
6774
  setFormsReady(true);
6767
6775
  }
6768
6776
  })();
6769
- const persistedConversation = conversationIdSig.value;
6770
- let resume = null;
6771
- if (persistedConversation) {
6772
- transport.primeIdentity(visitorId, persistedConversation);
6773
- resume = transport.resumeStream();
6774
- void runResume(resume);
6777
+ if (pendingThreadRef.current) {
6778
+ pendingThreadRef.current = false;
6779
+ const persisted = conversationIdSig.value;
6780
+ if (persisted) void loadThread(persisted);
6775
6781
  }
6776
- return () => {
6777
- connectGenRef.current++;
6778
- resume?.cancel();
6779
- };
6780
- }, []);
6782
+ if (conversationIdSig.value) {
6783
+ const handle = transport.resumeStream();
6784
+ resumeHandleRef.current = handle;
6785
+ void runResume(handle);
6786
+ }
6787
+ }, [activated]);
6781
6788
  const lastUserSig = useRef9(userSig);
6782
6789
  useEffect17(() => {
6783
6790
  if (!conversationReady || lastUserSig.current === userSig) return;
6784
6791
  lastUserSig.current = userSig;
6785
- void runStartConversation({ newConversation: false });
6786
- }, [userSig, conversationReady, runStartConversation]);
6792
+ void runHandshake({ newConversation: false });
6793
+ }, [userSig, conversationReady, runHandshake]);
6787
6794
  useEffect17(() => {
6788
6795
  applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
6789
6796
  }, [hostElement, isOpen, panelSize, options.mode]);
@@ -7010,10 +7017,10 @@ function App({ options, hostElement, bus }) {
7010
7017
  }, [transport, options.modules, unreadCountSig]);
7011
7018
  const unreadSeeded = useRef9(false);
7012
7019
  useEffect17(() => {
7013
- if (!conversationReady || unreadSeeded.current) return;
7020
+ if (!activated || !conversationReady || unreadSeeded.current) return;
7014
7021
  unreadSeeded.current = true;
7015
7022
  refreshUnread();
7016
- }, [conversationReady, refreshUnread]);
7023
+ }, [activated, conversationReady, refreshUnread]);
7017
7024
  const handleOpen = useCallback6(() => {
7018
7025
  log16.info("open", { mode: options.mode });
7019
7026
  feedback.unlock();
@@ -7035,8 +7042,8 @@ function App({ options, hostElement, bus }) {
7035
7042
  persistence.clearConversation();
7036
7043
  setCanSend(true);
7037
7044
  setFormMarkers([]);
7038
- void runStartConversation({ newConversation: true });
7039
- }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runStartConversation, bus]);
7045
+ void runHandshake({ newConversation: true });
7046
+ }, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
7040
7047
  const handleNewChat = useCallback6(() => {
7041
7048
  handleClear();
7042
7049
  setView("chat");
@@ -7452,7 +7459,7 @@ var TRACKED = {
7452
7459
  clear: () => void 0,
7453
7460
  suggestion: () => void 0,
7454
7461
  toggleHistory: (p33) => ({ view: p33.view }),
7455
- conversationStarted: () => void 0,
7462
+ handshake: () => void 0,
7456
7463
  // Forms + human-in-the-loop — ids and outcomes, never values.
7457
7464
  formSubmit: (p33) => ({ formId: p33.formId, skipped: p33.skipped }),
7458
7465
  toolResult: () => void 0,
@@ -7621,7 +7628,7 @@ var EVENT_NAMES = [
7621
7628
  "calloutDismiss",
7622
7629
  // Lifecycle / errors
7623
7630
  "error",
7624
- "conversationStarted",
7631
+ "handshake",
7625
7632
  "visitorRebound",
7626
7633
  "conversationRebound"
7627
7634
  ];
@@ -7678,7 +7685,7 @@ function mount(opts) {
7678
7685
  );
7679
7686
  };
7680
7687
  renderApp();
7681
- bus.on("conversationStarted", (response) => {
7688
+ bus.on("handshake", (response) => {
7682
7689
  if (!response.config) return;
7683
7690
  rawOptions = mergeServerConfig(rawOptions, response.config);
7684
7691
  currentOptions = resolveOptions(rawOptions);
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  ],
81
81
  "type": "module",
82
82
  "types": "./index.d.ts",
83
- "version": "0.25.0"
83
+ "version": "0.26.0"
84
84
  }