@xapp/chat-widget 1.100.0 → 1.102.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/dist/index.es.js CHANGED
@@ -919,6 +919,15 @@ var ActionBarCta = function (_a) {
919
919
  * in `_mixins.scss`. Keep this in step with the `mobile()` mixin. XappMedia/chat-widget#1594.
920
920
  */
921
921
  var MOBILE_MEDIA_QUERY = "(max-width: 640px), (max-height: 480px) and (pointer: coarse)";
922
+ /**
923
+ * Whether the chat panel renders full-screen right now. False where matchMedia is missing
924
+ * (jsdom), matching ActionBar's assume-desktop fallback.
925
+ */
926
+ function isMobileViewport(win) {
927
+ if (win === void 0) { win = window; }
928
+ return typeof win.matchMedia === "function" && win.matchMedia(MOBILE_MEDIA_QUERY).matches;
929
+ }
930
+
922
931
  /**
923
932
  * ChatWidget.scss sizes the chat panel against this variable. Nothing used to set it, so it always
924
933
  * fell back to the hardcoded 80px in the SCSS - and the real mobile bar is taller than that once
@@ -2322,8 +2331,100 @@ function setVisualStatus(status) {
2322
2331
  };
2323
2332
  }
2324
2333
 
2325
- function execute(url, dispatch, visuals, behavior) {
2334
+ /**
2335
+ * Carries "this visitor just clicked a link in the chat" across a same-tab navigation.
2336
+ *
2337
+ * Persisted widget state already survives the page load, but it cannot tell a chat link
2338
+ * click from a visitor coming back to the site later. The param can; it is read on arrival
2339
+ * and stripped from the address bar so it is never bookmarked or shared.
2340
+ */
2341
+ var CHAT_CONTINUE_PARAM = "xa_chat";
2342
+ var CHAT_CONTINUE_VALUE = "resume";
2343
+ var CONTINUE_CTA_MESSAGE = "Continue your conversation";
2344
+ /**
2345
+ * Marks a link the widget is about to follow in this tab. Only same-origin links: widget
2346
+ * state lives in this origin's localStorage, so the widget on any other origin - a
2347
+ * subdomain included - has no conversation to continue.
2348
+ */
2349
+ function withChatContinueParam(url, currentOrigin) {
2350
+ try {
2351
+ var urlObj = new URL(url);
2352
+ if (urlObj.origin !== currentOrigin || urlObj.searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE) {
2353
+ return url;
2354
+ }
2355
+ urlObj.searchParams.set(CHAT_CONTINUE_PARAM, CHAT_CONTINUE_VALUE);
2356
+ return urlObj.toString();
2357
+ }
2358
+ catch (_) {
2359
+ return url;
2360
+ }
2361
+ }
2362
+ /**
2363
+ * True when this page was reached through a chat link. Pure, so it is safe inside a
2364
+ * render-time initializer that React may call twice (StrictMode); strip the param
2365
+ * separately with `stripChatContinueParam`, from an effect.
2366
+ */
2367
+ function hasChatContinueParam(win) {
2368
+ if (win === void 0) { win = window; }
2369
+ try {
2370
+ return new URL(win.location.href).searchParams.get(CHAT_CONTINUE_PARAM) === CHAT_CONTINUE_VALUE;
2371
+ }
2372
+ catch (_) {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ /**
2377
+ * Removes the param from the address bar with replaceState, keeping the rest of the URL
2378
+ * and any SPA history state. A no-op when it is absent, so repeating it is harmless.
2379
+ */
2380
+ function stripChatContinueParam(win) {
2381
+ if (win === void 0) { win = window; }
2382
+ try {
2383
+ var url = new URL(win.location.href);
2384
+ if (!url.searchParams.has(CHAT_CONTINUE_PARAM)) {
2385
+ return;
2386
+ }
2387
+ url.searchParams.delete(CHAT_CONTINUE_PARAM);
2388
+ win.history.replaceState(win.history.state, "", url.toString());
2389
+ }
2390
+ catch (_) {
2391
+ // An address bar we cannot rewrite only leaves the param visible.
2392
+ }
2393
+ }
2394
+ /**
2395
+ * A conversation worth continuing: any message on screen. Join notices do not count - the
2396
+ * MCP connection posts one before the greeting, so a brand-new session has one too.
2397
+ */
2398
+ function hasConversation(chats) {
2399
+ return !!(chats === null || chats === void 0 ? void 0 : chats.some(function (chat) { return chat.type === "chat.msg"; }));
2400
+ }
2401
+ /**
2402
+ * The CTA to show, given whether there is a conversation to go back to.
2403
+ *
2404
+ * With one, the first-visit copy ("Need Help? Click Here") becomes a prompt to continue,
2405
+ * keeping the tenant's timing and styling. A tenant with no CTA gets none on an ordinary
2406
+ * page load; right after a chat link it does, immediately - on mobile that prompt is the
2407
+ * only sign the conversation is still there, because the panel stays closed over the page
2408
+ * they asked for.
2409
+ */
2410
+ function resolveCta(cta, options) {
2411
+ var arrivedViaChatLink = options.arrivedViaChatLink;
2412
+ if (!options.hasConversation || (!(cta === null || cta === void 0 ? void 0 : cta.message) && !arrivedViaChatLink)) {
2413
+ return cta;
2414
+ }
2415
+ var immediately = arrivedViaChatLink ? { delay: 0 } : {};
2416
+ var mobile = cta === null || cta === void 0 ? void 0 : cta.mobile;
2417
+ return __assign$1(__assign$1(__assign$1(__assign$1({}, cta), { message: CONTINUE_CTA_MESSAGE }), immediately), (mobile && {
2418
+ mobile: __assign$1(__assign$1(__assign$1({}, mobile), ((typeof mobile.message === "string" || arrivedViaChatLink) && { message: CONTINUE_CTA_MESSAGE })), immediately),
2419
+ }));
2420
+ }
2421
+
2422
+ /**
2423
+ * @internal Exported for unit testing; use `useOpenUrlCallback`.
2424
+ */
2425
+ function executeOpenUrl(url, dispatch, visuals, behavior, isMobile) {
2326
2426
  var _a, _b;
2427
+ if (isMobile === void 0) { isMobile = isMobileViewport(); }
2327
2428
  if (!dispatch || !visuals) {
2328
2429
  throw new Error("OpenUrl: No dispatch and/or visual.");
2329
2430
  }
@@ -2390,12 +2491,23 @@ function execute(url, dispatch, visuals, behavior) {
2390
2491
  window.open(url, "_blank");
2391
2492
  break;
2392
2493
  case "sameWindow":
2393
- if (!visuals.opened) {
2494
+ // Desktop: the next page reopens the chat beside the content, as before.
2495
+ // Mobile: the panel is full-screen, so reopening it would cover the page they
2496
+ // just asked for. Leave it closed; the param lets that page offer to continue.
2497
+ // Unconditional: callers can hand in a `visuals` captured before the chat opened
2498
+ // (ChatMarkdownMessage's click handler does), and skipping on a stale `opened`
2499
+ // leaves the persisted state open.
2500
+ if (isMobile) {
2501
+ dispatch(setVisualStatus({
2502
+ opened: false
2503
+ }));
2504
+ }
2505
+ else if (!visuals.opened) {
2394
2506
  dispatch(setVisualStatus({
2395
2507
  opened: true
2396
2508
  }));
2397
2509
  }
2398
- window.open(url, "_self");
2510
+ window.open(withChatContinueParam(url, window.location.origin), "_self");
2399
2511
  break;
2400
2512
  default:
2401
2513
  throwBadKind$1(type);
@@ -2407,7 +2519,7 @@ function useOpenUrlCallback() {
2407
2519
  if (!behavior) {
2408
2520
  behavior = (env === null || env === void 0 ? void 0 : env.urls) ? resolveUrlPolicy(url, env.urls) : defaultBehavior;
2409
2521
  }
2410
- execute(url, dispatch, visuals, behavior);
2522
+ executeOpenUrl(url, dispatch, visuals, behavior);
2411
2523
  }, [env]);
2412
2524
  }
2413
2525
 
@@ -4300,6 +4412,10 @@ var MCPChat = /** @class */ (function () {
4300
4412
  this.isInitializingSession = false;
4301
4413
  // Session validation state (silent handshake - no stream dispatch)
4302
4414
  this.isValidatingSession = false;
4415
+ // A conversation is on screen that the server should keep: restored from a previous page,
4416
+ // or confirmed by the server on this one. Sent as `continueConversation`, which also lets
4417
+ // the server resume a session idle longer than its default window.
4418
+ this.conversationOnScreen = false;
4303
4419
  // Tool call tracking for debug mode
4304
4420
  this.currentToolCalls = [];
4305
4421
  this.isReplaying = false;
@@ -4387,8 +4503,12 @@ var MCPChat = /** @class */ (function () {
4387
4503
  _a.label = 1;
4388
4504
  case 1:
4389
4505
  _a.trys.push([1, 3, 4, 5]);
4390
- return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload({ resumeSession: true }))];
4506
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4507
+ // server to continue it: without the flag it clears the history and greets.
4508
+ return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ resumeSession: true }, (this.conversationOnScreen && { continueConversation: true }))))];
4391
4509
  case 2:
4510
+ // With a conversation on screen (e.g. after a same-tab link click), ask the
4511
+ // server to continue it: without the flag it clears the history and greets.
4392
4512
  _a.sent();
4393
4513
  log("Session validation complete");
4394
4514
  return [3 /*break*/, 5];
@@ -4643,8 +4763,8 @@ var MCPChat = /** @class */ (function () {
4643
4763
  // Generic fallback
4644
4764
  return "Something went wrong. Please try again.";
4645
4765
  };
4646
- MCPChat.prototype.dispatchStreamStart = function (messageId) {
4647
- var bot = this.getBot(undefined);
4766
+ MCPChat.prototype.dispatchStreamStart = function (messageId, user) {
4767
+ var bot = this.getBot(user);
4648
4768
  var streamTimestamp = +new Date();
4649
4769
  log("[dispatchStreamStart] Stream started: ".concat(messageId, ", timestamp: ").concat(streamTimestamp));
4650
4770
  this.dispatch({
@@ -4657,30 +4777,25 @@ var MCPChat = /** @class */ (function () {
4657
4777
  },
4658
4778
  });
4659
4779
  };
4660
- MCPChat.prototype.dispatchStreamChunk = function (messageId, text) {
4780
+ MCPChat.prototype.dispatchStreamChunk = function (messageId, text, user) {
4661
4781
  this.dispatch({
4662
4782
  type: "chat",
4663
4783
  detail: {
4664
4784
  type: "chat.stream.chunk",
4665
- user: this.getBot(undefined),
4785
+ user: this.getBot(user),
4666
4786
  messageId: messageId,
4667
4787
  text: text,
4668
4788
  timestamp: +new Date(),
4669
4789
  },
4670
4790
  });
4671
4791
  };
4672
- MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, toolCalls) {
4792
+ MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, options) {
4793
+ if (options === void 0) { options = {}; }
4794
+ var toolCalls = options.toolCalls, user = options.user, preserveChips = options.preserveChips;
4673
4795
  log("Stream ended: ".concat(messageId));
4674
4796
  this.dispatch({
4675
4797
  type: "chat",
4676
- detail: {
4677
- type: "chat.stream.end",
4678
- user: this.getBot(undefined),
4679
- messageId: messageId,
4680
- msg: fullMessage,
4681
- toolCalls: toolCalls,
4682
- timestamp: +new Date(),
4683
- },
4798
+ detail: __assign$1(__assign$1({ type: "chat.stream.end", user: this.getBot(user), messageId: messageId, msg: fullMessage, toolCalls: toolCalls }, (preserveChips && { preserveChips: preserveChips })), { timestamp: +new Date() }),
4684
4799
  });
4685
4800
  };
4686
4801
  MCPChat.prototype.dispatchStreamAbort = function (messageId) {
@@ -4790,6 +4905,19 @@ var MCPChat = /** @class */ (function () {
4790
4905
  MCPChat.prototype.bufferStreamText = function (text) {
4791
4906
  this.streamBuffer += text;
4792
4907
  };
4908
+ /**
4909
+ * Dispatches whatever is buffered right now, without waiting on the throttle timer.
4910
+ *
4911
+ * Used when a bubble has to be closed mid-turn (`message_start`): the split has to be
4912
+ * synchronous, because frames arrive in order on one `onmessage` callback and deferring it
4913
+ * to a microtask would let the next `text` frame land in the bubble we are closing.
4914
+ */
4915
+ MCPChat.prototype.flushStreamBufferSync = function () {
4916
+ if (this.streamBuffer.length > 0 && this.streamingMessageIdForThrottle) {
4917
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4918
+ this.streamBuffer = "";
4919
+ }
4920
+ };
4793
4921
  /**
4794
4922
  * Flushes any remaining buffered text and stops the throttle timer.
4795
4923
  * Returns a promise that resolves when all buffered text has been dispatched.
@@ -5060,7 +5188,7 @@ var MCPChat = /** @class */ (function () {
5060
5188
  return [2 /*return*/];
5061
5189
  }
5062
5190
  log("[attemptSSERequest] Sending message: \"".concat(messageText, "\", userId: ").concat(this._userId, ", sessionId: ").concat(this._sessionId));
5063
- return [2 /*return*/, this.performSSERequest(this.buildRequestPayload({ message: messageText }))];
5191
+ return [2 /*return*/, this.performSSERequest(this.buildRequestPayload(__assign$1({ message: messageText }, (this.conversationOnScreen && { continueConversation: true }))))];
5064
5192
  });
5065
5193
  });
5066
5194
  };
@@ -5070,7 +5198,7 @@ var MCPChat = /** @class */ (function () {
5070
5198
  */
5071
5199
  MCPChat.prototype.performSSERequest = function (body) {
5072
5200
  return __awaiter$1(this, void 0, void 0, function () {
5073
- var endpoint, messageId, textChunks, hasError, httpStatusCode, sseError;
5201
+ var endpoint, messageId, bubbleId, bubbleUser, bubbleChunks, hasError, httpStatusCode, sseError;
5074
5202
  var _this = this;
5075
5203
  return __generator$1(this, function (_a) {
5076
5204
  endpoint = this.getEndpoint();
@@ -5083,7 +5211,8 @@ var MCPChat = /** @class */ (function () {
5083
5211
  this.abortController = new AbortController();
5084
5212
  messageId = uuid_1();
5085
5213
  this.currentMessageId = messageId;
5086
- textChunks = [];
5214
+ bubbleId = messageId;
5215
+ bubbleChunks = [];
5087
5216
  hasError = false;
5088
5217
  return [2 /*return*/, new Promise(function (resolve, reject) {
5089
5218
  // WARNING: Do not log headers object - it contains sensitive auth tokens
@@ -5114,6 +5243,7 @@ var MCPChat = /** @class */ (function () {
5114
5243
  });
5115
5244
  }); },
5116
5245
  onmessage: function (event) {
5246
+ var _a;
5117
5247
  // Guard: ignore events from stale/aborted requests
5118
5248
  if (_this.currentMessageId !== messageId) {
5119
5249
  log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
@@ -5140,10 +5270,15 @@ var MCPChat = /** @class */ (function () {
5140
5270
  case "connected":
5141
5271
  // Connection established
5142
5272
  log("[SSE] Connected event received, messageId: ".concat(messageId, ", isReturningUser: ").concat(data.isReturningUser, ", isReplay: ").concat(data.isReplay));
5273
+ _this.conversationOnScreen = true;
5143
5274
  // Track if this is a replay connection
5144
5275
  if (data.isReplay) {
5145
5276
  _this.isReplaying = true;
5146
5277
  }
5278
+ // A server may name the participant up front.
5279
+ if (data.user) {
5280
+ bubbleUser = data.user;
5281
+ }
5147
5282
  // Store identity from server response
5148
5283
  if (data.userId || data.sessionId) {
5149
5284
  if (data.userId) {
@@ -5163,18 +5298,35 @@ var MCPChat = /** @class */ (function () {
5163
5298
  if (data.isReplay) {
5164
5299
  // Replay connection — resume into the existing partial
5165
5300
  // message instead of creating a duplicate placeholder
5166
- _this.dispatchStreamResume(messageId);
5301
+ _this.dispatchStreamResume(bubbleId);
5167
5302
  }
5168
5303
  else {
5169
5304
  // Normal connection — create new streaming placeholder
5170
- _this.dispatchStreamStart(messageId);
5305
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5306
+ }
5307
+ _this.startStreamThrottle(bubbleId);
5308
+ break;
5309
+ case "session_resumed":
5310
+ // The server continued the conversation: no greeting follows,
5311
+ // so adopt the identity without opening a stream bubble.
5312
+ log("[SSE] Session resumed, sessionId: ".concat(data.sessionId, ", hasHistory: ").concat(data.hasHistory));
5313
+ _this.conversationOnScreen = true;
5314
+ if (data.userId) {
5315
+ _this._userId = data.userId;
5316
+ _this.userIdIsServerAssigned = true;
5317
+ }
5318
+ if (data.sessionId) {
5319
+ _this._sessionId = data.sessionId;
5320
+ _this.dispatch(setSessionId(_this._sessionId));
5171
5321
  }
5172
- _this.startStreamThrottle(messageId);
5322
+ storeIdentity(data.userId, data.sessionId);
5323
+ _this.stopTyping();
5173
5324
  break;
5174
5325
  case "session_expired":
5175
5326
  // Session no longer exists on server - need to start fresh
5176
5327
  log("[SSE] Session expired: ".concat(data.reason || "Session not found"));
5177
5328
  // Clear stored session (and lastSeq) but keep userId for returning user identification
5329
+ _this.conversationOnScreen = false;
5178
5330
  clearStoredSession(_this._sessionId);
5179
5331
  _this._sessionId = "";
5180
5332
  _this.lastSeq = undefined;
@@ -5187,14 +5339,40 @@ var MCPChat = /** @class */ (function () {
5187
5339
  _this.initializeNewSession();
5188
5340
  resolve();
5189
5341
  break;
5342
+ case "message_start": {
5343
+ // Close the bubble being streamed and open a new one, optionally
5344
+ // under a different participant. This is the only way a turn can
5345
+ // render as more than one message: the reducer creates a fresh
5346
+ // bubble per `chat.stream.start` and finalizes any still-streaming
5347
+ // one, so a server can make a handoff look like someone joining.
5348
+ // Servers that never send this are unaffected.
5349
+ log("[SSE] message_start \u2014 splitting bubble, user: ".concat((_a = data.user) === null || _a === void 0 ? void 0 : _a.displayName));
5350
+ _this.flushStreamBufferSync();
5351
+ _this.dispatchStreamEnd(bubbleId, { text: bubbleChunks.join("") }, {
5352
+ // Same debug-mode capture as `done`, scoped to this bubble:
5353
+ // stream.end replaces a bubble's toolCalls wholesale, so any
5354
+ // left in currentToolCalls would reappear on the next one.
5355
+ toolCalls: (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined,
5356
+ user: bubbleUser,
5357
+ // Chips belong to the turn; its final end sets them.
5358
+ preserveChips: true,
5359
+ });
5360
+ _this.currentToolCalls = [];
5361
+ bubbleId = uuid_1();
5362
+ bubbleChunks = [];
5363
+ bubbleUser = data.user;
5364
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5365
+ _this.startStreamThrottle(bubbleId);
5366
+ break;
5367
+ }
5190
5368
  case "text":
5191
5369
  if (data.text) {
5192
- log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5193
- textChunks.push(data.text);
5370
+ log("[SSE] Text chunk received (".concat(data.text.length, " chars), chunks in this bubble: ").concat(bubbleChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5371
+ bubbleChunks.push(data.text);
5194
5372
  if (data.isReplay || _this.isSleeping) {
5195
5373
  // Replayed text or tab hidden — dispatch immediately
5196
5374
  // (no throttle timer running in either case)
5197
- _this.dispatchStreamChunk(messageId, data.text);
5375
+ _this.dispatchStreamChunk(bubbleId, data.text, bubbleUser);
5198
5376
  }
5199
5377
  else {
5200
5378
  // Live text — buffer for throttled dispatch
@@ -5218,7 +5396,7 @@ var MCPChat = /** @class */ (function () {
5218
5396
  _this.currentToolCalls.push(toolCall);
5219
5397
  // Always dispatch tool start for user-friendly progress display
5220
5398
  _this.dispatchToolStart({
5221
- messageId: messageId,
5399
+ messageId: bubbleId,
5222
5400
  toolName: data.toolName,
5223
5401
  toolCallId: data.toolCallId,
5224
5402
  displayName: data.displayName,
@@ -5259,7 +5437,7 @@ var MCPChat = /** @class */ (function () {
5259
5437
  }
5260
5438
  // Always dispatch tool end for user-friendly progress display
5261
5439
  _this.dispatchToolEnd({
5262
- messageId: messageId,
5440
+ messageId: bubbleId,
5263
5441
  toolName: data.toolName,
5264
5442
  toolCallId: data.toolCallId,
5265
5443
  displayName: data.displayName,
@@ -5281,8 +5459,10 @@ var MCPChat = /** @class */ (function () {
5281
5459
  storeIdentity(undefined, data.response.sessionId);
5282
5460
  }
5283
5461
  // Wait for buffer to flush, then finalize
5284
- var accumulatedText = textChunks.join("");
5285
- log("[SSE] Done event received. Total chunks: ".concat(textChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5462
+ // The bubble's own text, not the whole turn's — a turn split by
5463
+ // `message_start` has already committed its earlier bubbles.
5464
+ var accumulatedText = bubbleChunks.join("");
5465
+ log("[SSE] Done event received. Chunks in this bubble: ".concat(bubbleChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5286
5466
  log("[SSE] First 100 chars of accumulated: ".concat(accumulatedText.substring(0, 100)));
5287
5467
  // Map suggestions to options (chips) format
5288
5468
  var suggestions = data.response.suggestions;
@@ -5298,8 +5478,14 @@ var MCPChat = /** @class */ (function () {
5298
5478
  if (sources === null || sources === void 0 ? void 0 : sources.length) {
5299
5479
  log("[SSE] Sources: ".concat(sources.length, " items"));
5300
5480
  }
5481
+ // `response.response` is request-scoped. For an unsplit turn it is
5482
+ // the whole answer, and the fallback serves servers that deliver it
5483
+ // only via `done`. Once `message_start` has fired it would copy every
5484
+ // earlier bubble's text into this one, so a split turn's bubbles
5485
+ // must stream their text as `text` frames.
5486
+ var isSplitTurn = bubbleId !== messageId;
5301
5487
  var finalMessage_1 = {
5302
- text: accumulatedText || data.response.response || "",
5488
+ text: accumulatedText || (isSplitTurn ? "" : data.response.response) || "",
5303
5489
  options: options,
5304
5490
  sources: sources,
5305
5491
  displays: data.response.displays,
@@ -5307,7 +5493,7 @@ var MCPChat = /** @class */ (function () {
5307
5493
  // Capture tool calls before flushing (debug mode — config or runtime toggle)
5308
5494
  var toolCalls_1 = (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
5309
5495
  _this.flushStreamBuffer().then(function () {
5310
- _this.dispatchStreamEnd(messageId, finalMessage_1, toolCalls_1);
5496
+ _this.dispatchStreamEnd(bubbleId, finalMessage_1, { toolCalls: toolCalls_1, user: bubbleUser });
5311
5497
  _this.currentToolCalls = []; // Clear after dispatch
5312
5498
  resolve();
5313
5499
  });
@@ -5347,7 +5533,7 @@ var MCPChat = /** @class */ (function () {
5347
5533
  log("[MCPChat] SSE error during sleep - will request replay on wakeup");
5348
5534
  _this.needsReplayOnWakeup = true;
5349
5535
  if (_this.streamingMessageIdForThrottle) {
5350
- _this.dispatchStreamAbort(messageId);
5536
+ _this.dispatchStreamAbort(bubbleId);
5351
5537
  }
5352
5538
  _this.stopStreamThrottle();
5353
5539
  }
@@ -5391,6 +5577,7 @@ var MCPChat = /** @class */ (function () {
5391
5577
  MCPChat.prototype.setVisitorInfo = function (visitorInfo, sessionId, cb) {
5392
5578
  this.visitorInfo = visitorInfo;
5393
5579
  this._attributes = __assign$1({}, this.visitorInfo.attributes);
5580
+ this.conversationOnScreen = visitorInfo.hasConversation === true;
5394
5581
  // do not set currentUrl if localhost
5395
5582
  var href = new URL(window.location.href);
5396
5583
  if (href === null || href === void 0 ? void 0 : href.host.toLowerCase().startsWith("localhost")) {
@@ -12905,7 +13092,21 @@ var ChatTextMessage = function (props) {
12905
13092
  var wasStreamed = message.wasStreamed;
12906
13093
  var showTail = agentMessage && !props.sibling && !isStreaming && !wasStreamed;
12907
13094
  var hasToolCalls = props.toolCalls && props.toolCalls.length > 0;
12908
- return (jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), jsx("div", { className: "chat-text-container", children: jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (jsx("span", { className: "chat-msg__content", dangerouslySetInnerHTML: { __html: parsedHtml } })) : (jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
13095
+ // Delegated, because the links are inside dangerouslySetInnerHTML. Modified clicks stay
13096
+ // with the browser so ctrl/cmd-click still opens a background tab.
13097
+ var onLinkClick = props.onLinkClick;
13098
+ var handleContentClick = function (ev) {
13099
+ var _a, _b;
13100
+ if (!onLinkClick || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) {
13101
+ return;
13102
+ }
13103
+ var link = (_b = (_a = ev.target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, "a");
13104
+ if (link && ev.currentTarget.contains(link)) {
13105
+ ev.preventDefault();
13106
+ onLinkClick(link.href);
13107
+ }
13108
+ };
13109
+ return (jsxs("div", { className: "chat-msg".concat(isStreaming ? " chat-msg--streaming" : ""), children: [hasToolCalls && (jsx(ToolCallDisplay, { toolCalls: props.toolCalls, debugMode: props.debugMode })), jsx("div", { className: "chat-text-container", children: jsxs(ChatMessageBubble, { owner: agentMessage ? "others" : "mine", hasTail: showTail, children: [jsx("span", { className: "message-sr-only", children: "at " + time + (agentMessage ? (isStreaming ? " the bot is typing" : " the bot said") : " the user said") }), parsedHtml ? (jsx("span", { className: "chat-msg__content", onClick: handleContentClick, dangerouslySetInnerHTML: { __html: parsedHtml } })) : (jsx("span", { className: "chat-msg__content", children: message.msg.text })), isStreaming && jsx("span", { className: "streaming-cursor", "aria-hidden": "true" })] }) })] }));
12909
13110
  };
12910
13111
 
12911
13112
  var ChatScheduleWidget = function (props) {
@@ -12948,14 +13149,21 @@ var avaKeys = ["text", "html", "card", "list"];
12948
13149
  * @returns
12949
13150
  */
12950
13151
  var ChatMessage = function (props) {
12951
- var _a, _b, _c;
13152
+ var _a, _b, _c, _d;
12952
13153
  var middleware = props.messageMiddleware || StandardMiddlewares;
12953
13154
  var chatConfig = useContext(ChatConfigContext);
12954
13155
  // console.log(`########### chatConfig: ${JSON.stringify(chatConfig, null, 2)}`);
12955
13156
  var ctx = props.middlewareContext;
12956
13157
  var user = props.agent;
13158
+ var chatDispatch = useChatDispatch();
13159
+ var visuals = useSelector(function (state) { return state.visuals; });
13160
+ // Text links follow the tenant's URL policy when it set one (utm_source, same-tab
13161
+ // continuation, ...). Tenants without one keep the plain new-tab anchor.
13162
+ var handleTextLinkClick = ((_a = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _a === void 0 ? void 0 : _a.urls) && (ctx === null || ctx === void 0 ? void 0 : ctx.openUrl)
13163
+ ? function (href) { return ctx.openUrl(href, chatDispatch, visuals); }
13164
+ : undefined;
12957
13165
  // Get hideUserInfo from the agent info based on the message sender's nick
12958
- var agentInfo = (_a = props.agents) === null || _a === void 0 ? void 0 : _a[props.message.user.nick];
13166
+ var agentInfo = (_b = props.agents) === null || _b === void 0 ? void 0 : _b[props.message.user.nick];
12959
13167
  var hideUserInfo = (agentInfo === null || agentInfo === void 0 ? void 0 : agentInfo.hideUserInfo) || false;
12960
13168
  function renderByType() {
12961
13169
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
@@ -12974,7 +13182,7 @@ var ChatMessage = function (props) {
12974
13182
  var debugMode = (_d = (_c = (_b = (_a = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.mcp) === null || _c === void 0 ? void 0 : _c.debugMode) !== null && _d !== void 0 ? _d : false;
12975
13183
  // Show tool progress with avatar when no text yet, or above text when text exists
12976
13184
  var showToolProgressWithAvatar = hasToolCalls && !msg.text;
12977
- return (jsxs(Fragment, { children: [showToolProgressWithAvatar && (jsx(ChatMessagePart, { showAvatar: true, user: user, avatarPosition: (_g = (_f = (_e = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _e === void 0 ? void 0 : _e.theme) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: true, children: jsx("div", { className: "chat-msg", children: jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (jsx(ChatMessagePart, { showAvatar: avaKey === "text", user: user, avatarPosition: (_k = (_j = (_h = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _h === void 0 ? void 0 : _h.theme) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: isStreaming_1 || wasStreamed, children: jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode }) })), msg.html &&
13185
+ return (jsxs(Fragment, { children: [showToolProgressWithAvatar && (jsx(ChatMessagePart, { showAvatar: true, user: user, avatarPosition: (_g = (_f = (_e = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _e === void 0 ? void 0 : _e.theme) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: true, children: jsx("div", { className: "chat-msg", children: jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (jsx(ChatMessagePart, { showAvatar: avaKey === "text", user: user, avatarPosition: (_k = (_j = (_h = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _h === void 0 ? void 0 : _h.theme) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k.avatarPosition, hideUserInfo: hideUserInfo, isStreaming: isStreaming_1 || wasStreamed, children: jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode, onLinkClick: handleTextLinkClick }) })), msg.html &&
12978
13186
  jsx(ChatMessagePart, { showAvatar: avaKey === "html", user: user, avatarPosition: (_o = (_m = (_l = chatConfig === null || chatConfig === void 0 ? void 0 : chatConfig.env) === null || _l === void 0 ? void 0 : _l.theme) === null || _m === void 0 ? void 0 : _m.messages) === null || _o === void 0 ? void 0 : _o.avatarPosition, hideUserInfo: hideUserInfo, children: jsx(ChatMarkdownMessage, { message: props.message, sibling: props.sibling, onOpenUrl: (_p = props.middlewareContext) === null || _p === void 0 ? void 0 : _p.openUrl }) }), msg.displays && middleware && msg.displays.map(function (display, index) {
12979
13187
  if (display.type === "ScheduleButton") {
12980
13188
  return (jsx(ChatScheduleWidget, { minimizeOnClick: props.minimizeOnClick, display: display }, index));
@@ -12998,7 +13206,7 @@ var ChatMessage = function (props) {
12998
13206
  }
12999
13207
  // Don't show timestamp/sources while streaming or if message has no content yet
13000
13208
  var isStreaming = props.message.isStreaming;
13001
- var hasContent = !!(((_b = props.message.msg) === null || _b === void 0 ? void 0 : _b.text) || ((_c = props.message.msg) === null || _c === void 0 ? void 0 : _c.html));
13209
+ var hasContent = !!(((_c = props.message.msg) === null || _c === void 0 ? void 0 : _c.text) || ((_d = props.message.msg) === null || _d === void 0 ? void 0 : _d.html));
13002
13210
  var showFooter = !isStreaming && hasContent;
13003
13211
  // empty
13004
13212
  return (jsx("div", { className: "chat-msg-container-wrapper ".concat(isAgent(props.message.user.nick) ? "agent" : "visitor", " ").concat(props.sibling ? "sibling" : ""), children: jsxs("div", { className: "chat-msg-container ".concat(getClassName(props.message)), children: [renderByType(), showFooter && renderTimestampAndSources()] }) }));
@@ -13008,9 +13216,6 @@ var ChatMessage = function (props) {
13008
13216
  * value must be like "60m", "2h" etc.
13009
13217
  */
13010
13218
  function convertToSeconds(s) {
13011
- if (!s) {
13012
- return 0;
13013
- }
13014
13219
  var groups = s.match(/(\d+)([dhms]?)/);
13015
13220
  if (!groups) {
13016
13221
  err("Invalid time string: ".concat(s));
@@ -13033,15 +13238,50 @@ function convertToSeconds(s) {
13033
13238
  }
13034
13239
  }
13035
13240
 
13036
- function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp) {
13037
- var durationMs = convertToSeconds(duration) * 1000;
13038
- // never if no session duration in config
13039
- if (!duration) {
13241
+ /**
13242
+ * How long a conversation must sit idle, on top of the calendar day changing, before it
13243
+ * expires when the widget config does not say.
13244
+ */
13245
+ var DEFAULT_SESSION_EXPIRATION = "4h";
13246
+ function isSameLocalDay(a, b) {
13247
+ var dayA = new Date(a);
13248
+ var dayB = new Date(b);
13249
+ return dayA.getFullYear() === dayB.getFullYear()
13250
+ && dayA.getMonth() === dayB.getMonth()
13251
+ && dayA.getDate() === dayB.getDate();
13252
+ }
13253
+ /**
13254
+ * A conversation expires only when BOTH the visitor's local calendar day has changed since
13255
+ * the last message AND at least `duration` has passed. So a visitor who browses the site
13256
+ * over an afternoon keeps one conversation, and one chatting at 11pm is not greeted afresh
13257
+ * at 1am - but tomorrow's visit starts clean.
13258
+ */
13259
+ function checkSessionExpiration(duration, lastMessageTimestamp, _lastTimestamp, now) {
13260
+ if (now === void 0) { now = Date.now(); }
13261
+ if (!lastMessageTimestamp) {
13040
13262
  return false;
13041
13263
  }
13042
- // TODO: Why? Let's use the time passed since the last message timestamp
13043
- // return lastTimestamp - lastMessageTimestamp > durationMs;
13044
- return +new Date() - lastMessageTimestamp > durationMs;
13264
+ var durationMs = convertToSeconds(duration || DEFAULT_SESSION_EXPIRATION) * 1000;
13265
+ return now - lastMessageTimestamp >= durationMs && !isSameLocalDay(lastMessageTimestamp, now);
13266
+ }
13267
+
13268
+ /**
13269
+ * Whether this page was reached through a same-tab link in the chat, read once on mount.
13270
+ *
13271
+ * The read is pure and the strip runs in an effect. StrictMode calls state initializers
13272
+ * twice in development, and React 18 (still allowed by our peer range) keeps the second
13273
+ * result - which an initializer that also stripped the param would get wrong. React 19
13274
+ * reuses the first result, so the test suite here cannot show that failure.
13275
+ */
13276
+ function useArrivedViaChatLink(disabled) {
13277
+ if (disabled === void 0) { disabled = false; }
13278
+ var arrived = useState(function () { return !disabled && hasChatContinueParam(); })[0];
13279
+ useEffect(function () {
13280
+ if (arrived) {
13281
+ stripChatContinueParam();
13282
+ }
13283
+ }, [arrived]);
13284
+ return arrived;
13045
13285
  }
13046
13286
 
13047
13287
  function useExternalScript(url) {
@@ -13162,6 +13402,7 @@ function useGreeting(active) {
13162
13402
  // Handle null context (e.g., in preview mode)
13163
13403
  var isAdmin = (_b = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.env) === null || _a === void 0 ? void 0 : _a.isAdmin) !== null && _b !== void 0 ? _b : false;
13164
13404
  var sessionId = useSelector(function (state) { return state.sessionId; });
13405
+ var conversationOnScreen = useSelector(function (state) { return hasConversation(state.chats); });
13165
13406
  useEffect(function () {
13166
13407
  if (active) {
13167
13408
  if (snapshotRef.current !== curr) {
@@ -13172,7 +13413,8 @@ function useGreeting(active) {
13172
13413
  visitorId: curr.visitorId,
13173
13414
  userId: curr.userId,
13174
13415
  accessToken: curr.accessToken,
13175
- attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href })
13416
+ attributes: __assign$1(__assign$1({}, curr.attributes), { currentUrl: window.location.href }),
13417
+ hasConversation: conversationOnScreen
13176
13418
  }, sessionId));
13177
13419
  if (!isAdmin) {
13178
13420
  var timeoutId_1 = setTimeout(function () {
@@ -37028,7 +37270,7 @@ var ChatWidgetConnected = function (props) {
37028
37270
  * Exported for use by StaticChatWidgetContainer to avoid infinite loops.
37029
37271
  */
37030
37272
  var ChatWidgetUI = function (props) {
37031
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13;
37273
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14;
37032
37274
  var innerDispatch = useChatDispatch();
37033
37275
  var dispatch = useChatServerDispatch();
37034
37276
  // Publish the visual viewport's geometry so the panel can size around the soft keyboard.
@@ -37049,14 +37291,18 @@ var ChatWidgetUI = function (props) {
37049
37291
  // Preview mode can be enabled via `preview` prop (recommended) or legacy `mode="preview"`
37050
37292
  var previewMode = props.preview === true || mode === "preview";
37051
37293
  var modeClass = "widget-container--".concat(mode);
37052
- var canRefresh = (_c = (_b = props.config.header) === null || _b === void 0 ? void 0 : _b.actions) === null || _c === void 0 ? void 0 : _c.refresh;
37294
+ // Reached this page through a link in the chat.
37295
+ var arrivedViaChatLink = useArrivedViaChatLink(previewMode);
37296
+ var conversationOnScreen = hasConversation(chatState.chats);
37297
+ var chatButtonCta = useMemo(function () { var _a; return resolveCta((_a = props.config) === null || _a === void 0 ? void 0 : _a.cta, { hasConversation: conversationOnScreen, arrivedViaChatLink: arrivedViaChatLink }); }, [(_b = props.config) === null || _b === void 0 ? void 0 : _b.cta, conversationOnScreen, arrivedViaChatLink]);
37298
+ var canRefresh = (_d = (_c = props.config.header) === null || _c === void 0 ? void 0 : _c.actions) === null || _d === void 0 ? void 0 : _d.refresh;
37053
37299
  // can't minimize in docked mode, static mode, or preview mode.
37054
- var canMinimize = !dockedMode && !staticMode && !previewMode && ((_e = (_d = props.config.header) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.minimize);
37055
- log("docked: ".concat(dockedMode, " static: ").concat(staticMode, " preview: ").concat(previewMode, " minimized: ").concat((_g = (_f = props.config.header) === null || _f === void 0 ? void 0 : _f.actions) === null || _g === void 0 ? void 0 : _g.minimize));
37300
+ var canMinimize = !dockedMode && !staticMode && !previewMode && ((_f = (_e = props.config.header) === null || _e === void 0 ? void 0 : _e.actions) === null || _f === void 0 ? void 0 : _f.minimize);
37301
+ log("docked: ".concat(dockedMode, " static: ").concat(staticMode, " preview: ").concat(previewMode, " minimized: ").concat((_h = (_g = props.config.header) === null || _g === void 0 ? void 0 : _g.actions) === null || _h === void 0 ? void 0 : _h.minimize));
37056
37302
  var canCancel;
37057
37303
  // To preserve legacy behavior, cancel needs a little more checks
37058
- if (typeof ((_j = (_h = props.config.header) === null || _h === void 0 ? void 0 : _h.actions) === null || _j === void 0 ? void 0 : _j.cancel) === "boolean") {
37059
- canCancel = !dockedMode && !staticMode && !previewMode && ((_l = (_k = props.config.header) === null || _k === void 0 ? void 0 : _k.actions) === null || _l === void 0 ? void 0 : _l.cancel);
37304
+ if (typeof ((_k = (_j = props.config.header) === null || _j === void 0 ? void 0 : _j.actions) === null || _k === void 0 ? void 0 : _k.cancel) === "boolean") {
37305
+ canCancel = !dockedMode && !staticMode && !previewMode && ((_m = (_l = props.config.header) === null || _l === void 0 ? void 0 : _l.actions) === null || _m === void 0 ? void 0 : _m.cancel);
37060
37306
  }
37061
37307
  else {
37062
37308
  canCancel = !dockedMode && !staticMode && !previewMode;
@@ -37067,16 +37313,16 @@ var ChatWidgetUI = function (props) {
37067
37313
  }
37068
37314
  // Our state - pull from storage
37069
37315
  // Parse autoOpenOnWidth once for use in both initial state and useEffect
37070
- var autoOpenOnWidthValue = ((_m = props.config) === null || _m === void 0 ? void 0 : _m.autoOpenOnWidth)
37316
+ var autoOpenOnWidthValue = ((_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnWidth)
37071
37317
  ? parseInt(props.config.autoOpenOnWidth, 10)
37072
37318
  : NaN;
37073
37319
  var autoOpenOnWidthMatch = !isNaN(autoOpenOnWidthValue) && window.innerWidth >= autoOpenOnWidthValue;
37074
37320
  // When autoOpenOnWidth is configured, use width check instead of persisted visible state
37075
37321
  // to prevent greeting requests on viewports below the threshold
37076
- var _14 = useState((!canMinimize && !canCancel) ||
37322
+ var _15 = useState((!canMinimize && !canCancel) ||
37077
37323
  // !!get("visible") ||
37078
- (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _14[0], setVisibleState = _14[1];
37079
- var _15 = useState(false); _15[0]; var setTypingState = _15[1]; // false initially - state kept for potential external observers
37324
+ (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _15[0], setVisibleState = _15[1];
37325
+ var _16 = useState(false); _16[0]; var setTypingState = _16[1]; // false initially - state kept for potential external observers
37080
37326
  // Ref to track typing state for use in timeout callbacks
37081
37327
  var typingRef = useRef(false);
37082
37328
  // Timeout ref for debouncing "stop typing" events
@@ -37084,11 +37330,11 @@ var ChatWidgetUI = function (props) {
37084
37330
  // Track whether autoOpenOnWidth has already fired so it doesn't re-open after user closes
37085
37331
  var autoOpenFiredRef = useRef(autoOpenOnWidthMatch);
37086
37332
  var chatServer = useContext(ChatServerContext);
37087
- var patternsConfig = (_p = (_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnPattern) === null || _p === void 0 ? void 0 : _p.patterns;
37333
+ var patternsConfig = (_q = (_p = props.config) === null || _p === void 0 ? void 0 : _p.autoOpenOnPattern) === null || _q === void 0 ? void 0 : _q.patterns;
37088
37334
  var currentUrl = window.location.href;
37089
37335
  var patternExist = patternsConfig && patternsConfig.length > 0;
37090
37336
  var patternMatches = patternsConfig === null || patternsConfig === void 0 ? void 0 : patternsConfig.some(function (pattern) { return currentUrl.includes(pattern); });
37091
- var configWidth = (_r = (_q = props.config) === null || _q === void 0 ? void 0 : _q.autoOpenOnPattern) === null || _r === void 0 ? void 0 : _r.minimumWidth;
37337
+ var configWidth = (_s = (_r = props.config) === null || _r === void 0 ? void 0 : _r.autoOpenOnPattern) === null || _s === void 0 ? void 0 : _s.minimumWidth;
37092
37338
  var currentWidth = window.innerWidth;
37093
37339
  var setVisible = useCallback(function (newVisible) {
37094
37340
  if (staticMode || previewMode) {
@@ -37123,7 +37369,7 @@ var ChatWidgetUI = function (props) {
37123
37369
  };
37124
37370
  // eslint-disable-next-line react-hooks/exhaustive-deps
37125
37371
  }, []);
37126
- var _16 = useState(!document.hidden), isTabVisible = _16[0], setIsTabVisible = _16[1];
37372
+ var _17 = useState(!document.hidden), isTabVisible = _17[0], setIsTabVisible = _17[1];
37127
37373
  useEffect(function () {
37128
37374
  var handleVisibilityChange = function () {
37129
37375
  setIsTabVisible(!document.hidden);
@@ -37347,8 +37593,8 @@ var ChatWidgetUI = function (props) {
37347
37593
  }));
37348
37594
  }
37349
37595
  // Action Bar state and handlers
37350
- var _17 = useState(false), formModalOpen = _17[0], setFormModalOpen = _17[1];
37351
- var _18 = useState(null), activeFormConfig = _18[0], setActiveFormConfig = _18[1];
37596
+ var _18 = useState(false), formModalOpen = _18[0], setFormModalOpen = _18[1];
37597
+ var _19 = useState(null), activeFormConfig = _19[0], setActiveFormConfig = _19[1];
37352
37598
  var handleFormButtonClick = useCallback(function (formButton) {
37353
37599
  // If form-widget is configured, use its openForm API
37354
37600
  // The form-widget script should already be pre-loaded
@@ -37415,7 +37661,7 @@ var ChatWidgetUI = function (props) {
37415
37661
  var messages = chatState && chatState.chats;
37416
37662
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
37417
37663
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
37418
- var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
37664
+ var actionBarEnabled = ((_t = config === null || config === void 0 ? void 0 : config.actionBar) === null || _t === void 0 ? void 0 : _t.enabled) === true;
37419
37665
  // The host page declares this; the widget cannot work it out. On the standalone
37420
37666
  // contact/booking page the form already *is* the page, which makes every button that
37421
37667
  // only opens one redundant. XappMedia/chat-widget#1626.
@@ -37439,7 +37685,7 @@ var ChatWidgetUI = function (props) {
37439
37685
  onConnectionStatusChange(connectionStatus);
37440
37686
  }
37441
37687
  }, [connectionStatus, onConnectionStatusChange]);
37442
- useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
37688
+ useExternalScript((_u = props.config) === null || _u === void 0 ? void 0 : _u.middlewareUrl);
37443
37689
  // Only reach for form.xapp.ai when something on this page can still open the form.
37444
37690
  //
37445
37691
  // Loading it is not free: the script mounts a second form-widget instance whose wrapper
@@ -37479,15 +37725,18 @@ var ChatWidgetUI = function (props) {
37479
37725
  };
37480
37726
  }, [actionBarVisible]);
37481
37727
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
37482
- var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
37728
+ var widgetAgent = ((_v = chatState.agents["agent:robot"]) === null || _v === void 0 ? void 0 : _v.user) ||
37483
37729
  (config === null || config === void 0 ? void 0 : config.agent) || {
37484
37730
  nick: "agent:robot",
37485
37731
  avatarPath: config.avatarUrl,
37486
37732
  display_name: "Agent",
37487
37733
  };
37488
- return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_x = (_w = (_v = props.config) === null || _v === void 0 ? void 0 : _v.connection) === null || _w === void 0 ? void 0 : _w.mcp) === null || _x === void 0 ? void 0 : _x.debugMode, onSubmit: handleOnSubmit }), jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_z = (_y = props.config) === null || _y === void 0 ? void 0 : _y.typingStatus) === null || _z === void 0 ? void 0 : _z.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_0 = config === null || config === void 0 ? void 0 : config.input) === null || _0 === void 0 ? void 0 : _0.placeholder, sendButtonIcon: (_2 = (_1 = config === null || config === void 0 ? void 0 : config.footer) === null || _1 === void 0 ? void 0 : _1.sendButton) === null || _2 === void 0 ? void 0 : _2.icon, sendButtonIconHover: (_4 = (_3 = config === null || config === void 0 ? void 0 : config.footer) === null || _3 === void 0 ? void 0 : _3.sendButton) === null || _4 === void 0 ? void 0 : _4.iconHover, sendButtonIconDisabled: (_6 = (_5 = config === null || config === void 0 ? void 0 : config.footer) === null || _5 === void 0 ? void 0 : _5.sendButton) === null || _6 === void 0 ? void 0 : _6.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_7 = props.config) === null || _7 === void 0 ? void 0 : _7.menu, footerConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.footer, inputConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37734
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarVisible ? " widget-container--with-action-bar" : ""), children: [jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), jsx(ChatHeader, { accountStatus: chatState.accountStatus, refreshOnClick: handleRestartClick, minimizeOnClick: handleMinimizeClick, cancelOnClick: handleCancelClick, agent: widgetAgent, canRefresh: canRefresh, canMinimize: canMinimize, canCancel: canCancel, config: config === null || config === void 0 ? void 0 : config.header, menuConfig: config.menu, debugMode: (_y = (_x = (_w = props.config) === null || _w === void 0 ? void 0 : _w.connection) === null || _x === void 0 ? void 0 : _x.mcp) === null || _y === void 0 ? void 0 : _y.debugMode, onSubmit: handleOnSubmit }), jsx(MessageList, { visible: visible, queuePosition: chatState.queuePosition, isChatting: chatState.isChatting, isOffline: isOffline, messages: messages, agents: chatState.agents, agent: config === null || config === void 0 ? void 0 : config.agent, lastRatingRequestTimestamp: chatState.lastRatingRequestTimestamp, hasRating: chatState.hasRating, visitorId: chatState.visitorId, hasWsButton: !!chatState.wsButton, messageMiddleware: props.messageMiddleware, textTypingStatusEnabled: (_0 = (_z = props.config) === null || _z === void 0 ? void 0 : _z.typingStatus) === null || _0 === void 0 ? void 0 : _0.textTypingStatusEnabled, disableAutoScroll: props.disableAutoScroll, onSend: handleSendMessage, onWrite: handleWriteMessage, onOpenUrl: handleOpenUrl, minimizeOnClick: handleMinimizeClick }), jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && jsx(ServerOffline, {}), chatState.wsButton && visible && (jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), jsx(ChatFooter, { isAdmin: config === null || config === void 0 ? void 0 : config.isAdmin, isChatting: chatState.isChatting, placeholder: (_1 = config === null || config === void 0 ? void 0 : config.input) === null || _1 === void 0 ? void 0 : _1.placeholder, sendButtonIcon: (_3 = (_2 = config === null || config === void 0 ? void 0 : config.footer) === null || _2 === void 0 ? void 0 : _2.sendButton) === null || _3 === void 0 ? void 0 : _3.icon, sendButtonIconHover: (_5 = (_4 = config === null || config === void 0 ? void 0 : config.footer) === null || _4 === void 0 ? void 0 : _4.sendButton) === null || _5 === void 0 ? void 0 : _5.iconHover, sendButtonIconDisabled: (_7 = (_6 = config === null || config === void 0 ? void 0 : config.footer) === null || _6 === void 0 ? void 0 : _6.sendButton) === null || _7 === void 0 ? void 0 : _7.iconDisabled, visible: visible, hasWsButton: !!chatState.wsButton, menuConfig: (_8 = props.config) === null || _8 === void 0 ? void 0 : _8.menu, footerConfig: (_9 = props.config) === null || _9 === void 0 ? void 0 : _9.footer, inputConfig: (_10 = props.config) === null || _10 === void 0 ? void 0 : _10.input, disabled: previewMode, onChange: handleOnChange, onSubmit: handleOnSubmit, onFileUpload: handleFileUpload }), jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarVisible && config.actionBar ? (jsxs(Fragment, { children: [jsx(ActionBar, { config: __assign$1(__assign$1({}, config.actionBar), {
37489
37735
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
37490
- cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, imageUrl: (_12 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _12 === void 0 ? void 0 : _12.imageUrl, visible: visible, hasInteracted: (_13 = chatState.visuals) === null || _13 === void 0 ? void 0 : _13.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37736
+ cta: resolveCta((_11 = config.actionBar.cta) !== null && _11 !== void 0 ? _11 : config.cta, {
37737
+ hasConversation: conversationOnScreen,
37738
+ arrivedViaChatLink: arrivedViaChatLink,
37739
+ }) }), visible: visible, chatDisabled: config.disabled, formDisabled: isFormPage, hasUserInteracted: (_12 = chatState.visuals) === null || _12 === void 0 ? void 0 : _12.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: chatButtonCta, imageUrl: (_13 = config === null || config === void 0 ? void 0 : config.chatButton) === null || _13 === void 0 ? void 0 : _13.imageUrl, visible: visible, hasInteracted: (_14 = chatState.visuals) === null || _14 === void 0 ? void 0 : _14.hasInteracted, onCtaDismiss: handleCtaDismiss })), jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
37491
37740
  };
37492
37741
  /**
37493
37742
  * Top-level wrapper that dispatches between preview mode and connected mode.
@@ -37947,8 +38196,11 @@ function update(state, action) {
37947
38196
  var finalMsg_1 = endDetail.msg;
37948
38197
  // Preserve tool calls from either the action or the existing message
37949
38198
  var toolCalls_1 = endDetail.toolCalls || endStreamingChat.toolCalls;
37950
- // Update chips from message options (same as chat.msg handler)
37951
- var chips = ((_w = finalMsg_1 === null || finalMsg_1 === void 0 ? void 0 : finalMsg_1.options) === null || _w === void 0 ? void 0 : _w.length) ? finalMsg_1.options : [];
38199
+ // Update chips from message options (same as chat.msg handler) — unless this closes
38200
+ // an intermediate bubble of a split turn, whose chips arrive with the final end.
38201
+ var chips = endDetail.preserveChips
38202
+ ? state.chips
38203
+ : ((_w = finalMsg_1 === null || finalMsg_1 === void 0 ? void 0 : finalMsg_1.options) === null || _w === void 0 ? void 0 : _w.length) ? finalMsg_1.options : [];
37952
38204
  return __assign$1(__assign$1({}, state), { lastTimestamp: endDetail === null || endDetail === void 0 ? void 0 : endDetail.timestamp, streamingMessageId: undefined, chips: chips, chats: state.chats.map(function (chat, index) {
37953
38205
  if (index === streamingIndex_4 && isChatMsgDetail(chat)) {
37954
38206
  return __assign$1(__assign$1({}, chat), { msg: finalMsg_1, isStreaming: false, wasStreamed: true, toolCalls: toolCalls_1 });