@xapp/chat-widget 1.99.0 → 1.101.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.js CHANGED
@@ -4644,8 +4644,8 @@ var MCPChat = /** @class */ (function () {
4644
4644
  // Generic fallback
4645
4645
  return "Something went wrong. Please try again.";
4646
4646
  };
4647
- MCPChat.prototype.dispatchStreamStart = function (messageId) {
4648
- var bot = this.getBot(undefined);
4647
+ MCPChat.prototype.dispatchStreamStart = function (messageId, user) {
4648
+ var bot = this.getBot(user);
4649
4649
  var streamTimestamp = +new Date();
4650
4650
  log("[dispatchStreamStart] Stream started: ".concat(messageId, ", timestamp: ").concat(streamTimestamp));
4651
4651
  this.dispatch({
@@ -4658,30 +4658,25 @@ var MCPChat = /** @class */ (function () {
4658
4658
  },
4659
4659
  });
4660
4660
  };
4661
- MCPChat.prototype.dispatchStreamChunk = function (messageId, text) {
4661
+ MCPChat.prototype.dispatchStreamChunk = function (messageId, text, user) {
4662
4662
  this.dispatch({
4663
4663
  type: "chat",
4664
4664
  detail: {
4665
4665
  type: "chat.stream.chunk",
4666
- user: this.getBot(undefined),
4666
+ user: this.getBot(user),
4667
4667
  messageId: messageId,
4668
4668
  text: text,
4669
4669
  timestamp: +new Date(),
4670
4670
  },
4671
4671
  });
4672
4672
  };
4673
- MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, toolCalls) {
4673
+ MCPChat.prototype.dispatchStreamEnd = function (messageId, fullMessage, options) {
4674
+ if (options === void 0) { options = {}; }
4675
+ var toolCalls = options.toolCalls, user = options.user, preserveChips = options.preserveChips;
4674
4676
  log("Stream ended: ".concat(messageId));
4675
4677
  this.dispatch({
4676
4678
  type: "chat",
4677
- detail: {
4678
- type: "chat.stream.end",
4679
- user: this.getBot(undefined),
4680
- messageId: messageId,
4681
- msg: fullMessage,
4682
- toolCalls: toolCalls,
4683
- timestamp: +new Date(),
4684
- },
4679
+ 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() }),
4685
4680
  });
4686
4681
  };
4687
4682
  MCPChat.prototype.dispatchStreamAbort = function (messageId) {
@@ -4791,6 +4786,19 @@ var MCPChat = /** @class */ (function () {
4791
4786
  MCPChat.prototype.bufferStreamText = function (text) {
4792
4787
  this.streamBuffer += text;
4793
4788
  };
4789
+ /**
4790
+ * Dispatches whatever is buffered right now, without waiting on the throttle timer.
4791
+ *
4792
+ * Used when a bubble has to be closed mid-turn (`message_start`): the split has to be
4793
+ * synchronous, because frames arrive in order on one `onmessage` callback and deferring it
4794
+ * to a microtask would let the next `text` frame land in the bubble we are closing.
4795
+ */
4796
+ MCPChat.prototype.flushStreamBufferSync = function () {
4797
+ if (this.streamBuffer.length > 0 && this.streamingMessageIdForThrottle) {
4798
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4799
+ this.streamBuffer = "";
4800
+ }
4801
+ };
4794
4802
  /**
4795
4803
  * Flushes any remaining buffered text and stops the throttle timer.
4796
4804
  * Returns a promise that resolves when all buffered text has been dispatched.
@@ -5071,7 +5079,7 @@ var MCPChat = /** @class */ (function () {
5071
5079
  */
5072
5080
  MCPChat.prototype.performSSERequest = function (body) {
5073
5081
  return __awaiter$1(this, void 0, void 0, function () {
5074
- var endpoint, messageId, textChunks, hasError, httpStatusCode, sseError;
5082
+ var endpoint, messageId, bubbleId, bubbleUser, bubbleChunks, hasError, httpStatusCode, sseError;
5075
5083
  var _this = this;
5076
5084
  return __generator$1(this, function (_a) {
5077
5085
  endpoint = this.getEndpoint();
@@ -5084,7 +5092,8 @@ var MCPChat = /** @class */ (function () {
5084
5092
  this.abortController = new AbortController();
5085
5093
  messageId = uuid_1();
5086
5094
  this.currentMessageId = messageId;
5087
- textChunks = [];
5095
+ bubbleId = messageId;
5096
+ bubbleChunks = [];
5088
5097
  hasError = false;
5089
5098
  return [2 /*return*/, new Promise(function (resolve, reject) {
5090
5099
  // WARNING: Do not log headers object - it contains sensitive auth tokens
@@ -5115,6 +5124,7 @@ var MCPChat = /** @class */ (function () {
5115
5124
  });
5116
5125
  }); },
5117
5126
  onmessage: function (event) {
5127
+ var _a;
5118
5128
  // Guard: ignore events from stale/aborted requests
5119
5129
  if (_this.currentMessageId !== messageId) {
5120
5130
  log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
@@ -5145,6 +5155,10 @@ var MCPChat = /** @class */ (function () {
5145
5155
  if (data.isReplay) {
5146
5156
  _this.isReplaying = true;
5147
5157
  }
5158
+ // A server may name the participant up front.
5159
+ if (data.user) {
5160
+ bubbleUser = data.user;
5161
+ }
5148
5162
  // Store identity from server response
5149
5163
  if (data.userId || data.sessionId) {
5150
5164
  if (data.userId) {
@@ -5164,13 +5178,13 @@ var MCPChat = /** @class */ (function () {
5164
5178
  if (data.isReplay) {
5165
5179
  // Replay connection — resume into the existing partial
5166
5180
  // message instead of creating a duplicate placeholder
5167
- _this.dispatchStreamResume(messageId);
5181
+ _this.dispatchStreamResume(bubbleId);
5168
5182
  }
5169
5183
  else {
5170
5184
  // Normal connection — create new streaming placeholder
5171
- _this.dispatchStreamStart(messageId);
5185
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5172
5186
  }
5173
- _this.startStreamThrottle(messageId);
5187
+ _this.startStreamThrottle(bubbleId);
5174
5188
  break;
5175
5189
  case "session_expired":
5176
5190
  // Session no longer exists on server - need to start fresh
@@ -5188,14 +5202,40 @@ var MCPChat = /** @class */ (function () {
5188
5202
  _this.initializeNewSession();
5189
5203
  resolve();
5190
5204
  break;
5205
+ case "message_start": {
5206
+ // Close the bubble being streamed and open a new one, optionally
5207
+ // under a different participant. This is the only way a turn can
5208
+ // render as more than one message: the reducer creates a fresh
5209
+ // bubble per `chat.stream.start` and finalizes any still-streaming
5210
+ // one, so a server can make a handoff look like someone joining.
5211
+ // Servers that never send this are unaffected.
5212
+ log("[SSE] message_start \u2014 splitting bubble, user: ".concat((_a = data.user) === null || _a === void 0 ? void 0 : _a.displayName));
5213
+ _this.flushStreamBufferSync();
5214
+ _this.dispatchStreamEnd(bubbleId, { text: bubbleChunks.join("") }, {
5215
+ // Same debug-mode capture as `done`, scoped to this bubble:
5216
+ // stream.end replaces a bubble's toolCalls wholesale, so any
5217
+ // left in currentToolCalls would reappear on the next one.
5218
+ toolCalls: (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined,
5219
+ user: bubbleUser,
5220
+ // Chips belong to the turn; its final end sets them.
5221
+ preserveChips: true,
5222
+ });
5223
+ _this.currentToolCalls = [];
5224
+ bubbleId = uuid_1();
5225
+ bubbleChunks = [];
5226
+ bubbleUser = data.user;
5227
+ _this.dispatchStreamStart(bubbleId, bubbleUser);
5228
+ _this.startStreamThrottle(bubbleId);
5229
+ break;
5230
+ }
5191
5231
  case "text":
5192
5232
  if (data.text) {
5193
- log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5194
- textChunks.push(data.text);
5233
+ log("[SSE] Text chunk received (".concat(data.text.length, " chars), chunks in this bubble: ").concat(bubbleChunks.length + 1, ", isReplay: ").concat(data.isReplay));
5234
+ bubbleChunks.push(data.text);
5195
5235
  if (data.isReplay || _this.isSleeping) {
5196
5236
  // Replayed text or tab hidden — dispatch immediately
5197
5237
  // (no throttle timer running in either case)
5198
- _this.dispatchStreamChunk(messageId, data.text);
5238
+ _this.dispatchStreamChunk(bubbleId, data.text, bubbleUser);
5199
5239
  }
5200
5240
  else {
5201
5241
  // Live text — buffer for throttled dispatch
@@ -5219,7 +5259,7 @@ var MCPChat = /** @class */ (function () {
5219
5259
  _this.currentToolCalls.push(toolCall);
5220
5260
  // Always dispatch tool start for user-friendly progress display
5221
5261
  _this.dispatchToolStart({
5222
- messageId: messageId,
5262
+ messageId: bubbleId,
5223
5263
  toolName: data.toolName,
5224
5264
  toolCallId: data.toolCallId,
5225
5265
  displayName: data.displayName,
@@ -5260,7 +5300,7 @@ var MCPChat = /** @class */ (function () {
5260
5300
  }
5261
5301
  // Always dispatch tool end for user-friendly progress display
5262
5302
  _this.dispatchToolEnd({
5263
- messageId: messageId,
5303
+ messageId: bubbleId,
5264
5304
  toolName: data.toolName,
5265
5305
  toolCallId: data.toolCallId,
5266
5306
  displayName: data.displayName,
@@ -5282,8 +5322,10 @@ var MCPChat = /** @class */ (function () {
5282
5322
  storeIdentity(undefined, data.response.sessionId);
5283
5323
  }
5284
5324
  // Wait for buffer to flush, then finalize
5285
- var accumulatedText = textChunks.join("");
5286
- log("[SSE] Done event received. Total chunks: ".concat(textChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5325
+ // The bubble's own text, not the whole turn's — a turn split by
5326
+ // `message_start` has already committed its earlier bubbles.
5327
+ var accumulatedText = bubbleChunks.join("");
5328
+ log("[SSE] Done event received. Chunks in this bubble: ".concat(bubbleChunks.length, ", accumulated text length: ").concat(accumulatedText.length));
5287
5329
  log("[SSE] First 100 chars of accumulated: ".concat(accumulatedText.substring(0, 100)));
5288
5330
  // Map suggestions to options (chips) format
5289
5331
  var suggestions = data.response.suggestions;
@@ -5299,8 +5341,14 @@ var MCPChat = /** @class */ (function () {
5299
5341
  if (sources === null || sources === void 0 ? void 0 : sources.length) {
5300
5342
  log("[SSE] Sources: ".concat(sources.length, " items"));
5301
5343
  }
5344
+ // `response.response` is request-scoped. For an unsplit turn it is
5345
+ // the whole answer, and the fallback serves servers that deliver it
5346
+ // only via `done`. Once `message_start` has fired it would copy every
5347
+ // earlier bubble's text into this one, so a split turn's bubbles
5348
+ // must stream their text as `text` frames.
5349
+ var isSplitTurn = bubbleId !== messageId;
5302
5350
  var finalMessage_1 = {
5303
- text: accumulatedText || data.response.response || "",
5351
+ text: accumulatedText || (isSplitTurn ? "" : data.response.response) || "",
5304
5352
  options: options,
5305
5353
  sources: sources,
5306
5354
  displays: data.response.displays,
@@ -5308,7 +5356,7 @@ var MCPChat = /** @class */ (function () {
5308
5356
  // Capture tool calls before flushing (debug mode — config or runtime toggle)
5309
5357
  var toolCalls_1 = (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
5310
5358
  _this.flushStreamBuffer().then(function () {
5311
- _this.dispatchStreamEnd(messageId, finalMessage_1, toolCalls_1);
5359
+ _this.dispatchStreamEnd(bubbleId, finalMessage_1, { toolCalls: toolCalls_1, user: bubbleUser });
5312
5360
  _this.currentToolCalls = []; // Clear after dispatch
5313
5361
  resolve();
5314
5362
  });
@@ -5348,7 +5396,7 @@ var MCPChat = /** @class */ (function () {
5348
5396
  log("[MCPChat] SSE error during sleep - will request replay on wakeup");
5349
5397
  _this.needsReplayOnWakeup = true;
5350
5398
  if (_this.streamingMessageIdForThrottle) {
5351
- _this.dispatchStreamAbort(messageId);
5399
+ _this.dispatchStreamAbort(bubbleId);
5352
5400
  }
5353
5401
  _this.stopStreamThrottle();
5354
5402
  }
@@ -12978,7 +13026,7 @@ var ChatMessage = function (props) {
12978
13026
  return (require$$0.jsxs(require$$0.Fragment, { children: [showToolProgressWithAvatar && (require$$0.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: require$$0.jsx("div", { className: "chat-msg", children: require$$0.jsx(ToolCallDisplay, { toolCalls: toolCalls, debugMode: debugMode }) }) })), msg.text && (require$$0.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: require$$0.jsx(ChatTextMessage, { message: props.message, sibling: props.sibling, user: user, toolCalls: hasToolCalls ? toolCalls : undefined, debugMode: debugMode }) })), msg.html &&
12979
13027
  require$$0.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: require$$0.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) {
12980
13028
  if (display.type === "ScheduleButton") {
12981
- return (require$$0.jsx(ChatScheduleWidget, { minimizeOnClick: props.minimizeOnClick, display: display }));
13029
+ return (require$$0.jsx(ChatScheduleWidget, { minimizeOnClick: props.minimizeOnClick, display: display }, index));
12982
13030
  }
12983
13031
  var Middleware = middleware;
12984
13032
  return (require$$0.jsx(Middleware, { msg: display, ctx: props.middlewareContext }, index));
@@ -13476,11 +13524,14 @@ var Suggestions = function (props) {
13476
13524
  var actions = React.useMemo(function () { return createActions(onItemUse); }, [onItemUse]);
13477
13525
  var transformContent = function (content) {
13478
13526
  var lines = content.split('\n');
13479
- return lines.map(function (line) {
13527
+ // Keyed by position: the lines have no id of their own, and a blank line between
13528
+ // paragraphs is not distinguishable by content either. `<></>` cannot carry a key,
13529
+ // hence the explicit Fragment.
13530
+ return lines.map(function (line, index) {
13480
13531
  if (line) {
13481
- return require$$0.jsx(MarkdownBuilder, { text: line });
13532
+ return require$$0.jsx(MarkdownBuilder, { text: line }, "line-".concat(index));
13482
13533
  }
13483
- return require$$0.jsx(require$$0.Fragment, {});
13534
+ return require$$0.jsx(React.Fragment, {}, "line-".concat(index));
13484
13535
  });
13485
13536
  };
13486
13537
  var containerClass = "xappw-suggestions ".concat(props.className || "", " ").concat(props.hasWsButton ? "xappw-suggestions--with-ws-button" : "").trim();
@@ -35471,13 +35522,26 @@ var MessageList = function (_a) {
35471
35522
  * until the user scrolled back by hand. Device, 2026-09-10.
35472
35523
  *
35473
35524
  * A container scroll cannot touch an ancestor, which is the entire point.
35525
+ *
35526
+ * `scrollTo` is feature-detected rather than assumed. It is absent in jsdom, so every
35527
+ * test that mounts this component through a passive effect throws on it -- but the
35528
+ * reason to guard is not the test runner: this widget renders on whatever browser a
35529
+ * customer's visitor brought, and a missing `Element.prototype.scrollTo` would throw
35530
+ * inside a passive effect and take the message list down with it. Assigning
35531
+ * `scrollTop` is the universally supported equivalent; it loses the smooth animation
35532
+ * and nothing else.
35474
35533
  */
35475
35534
  var scrollToBottom = React.useCallback(function (behavior) {
35476
35535
  if (behavior === void 0) { behavior = "smooth"; }
35477
35536
  var container = containerRef.current;
35478
- if (container) {
35537
+ if (!container) {
35538
+ return;
35539
+ }
35540
+ if (typeof container.scrollTo === "function") {
35479
35541
  container.scrollTo({ top: container.scrollHeight, behavior: behavior });
35542
+ return;
35480
35543
  }
35544
+ container.scrollTop = container.scrollHeight;
35481
35545
  }, []);
35482
35546
  // Always scroll to bottom when messages change (unless disabled)
35483
35547
  React.useEffect(function () {
@@ -35568,7 +35632,7 @@ var MessageList = function (_a) {
35568
35632
  case "chat.prechat":
35569
35633
  return require$$0.jsx(PrechatFormContainer, { time: time }, "prechat");
35570
35634
  default:
35571
- return (require$$0.jsxs("span", { children: [require$$0.jsx("span", { className: "message-sr-only", children: "at " + time + " system message" }), require$$0.jsxs("div", { children: ["Unhandled message: ", JSON.stringify(msg)] }, "uhm-".concat(+new Date()))] }, "default-".concat(msg.timestamp)));
35635
+ return (require$$0.jsxs("span", { children: [require$$0.jsx("span", { className: "message-sr-only", children: "at " + time + " system message" }), require$$0.jsxs("div", { "data-testid": "unhandled-message", children: ["Unhandled message: ", JSON.stringify(msg)] }, "uhm-".concat(msg.timestamp))] }, "default-".concat(msg.timestamp)));
35572
35636
  }
35573
35637
  }
35574
35638
  /**
@@ -37932,8 +37996,11 @@ function update(state, action) {
37932
37996
  var finalMsg_1 = endDetail.msg;
37933
37997
  // Preserve tool calls from either the action or the existing message
37934
37998
  var toolCalls_1 = endDetail.toolCalls || endStreamingChat.toolCalls;
37935
- // Update chips from message options (same as chat.msg handler)
37936
- 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 : [];
37999
+ // Update chips from message options (same as chat.msg handler) — unless this closes
38000
+ // an intermediate bubble of a split turn, whose chips arrive with the final end.
38001
+ var chips = endDetail.preserveChips
38002
+ ? state.chips
38003
+ : ((_w = finalMsg_1 === null || finalMsg_1 === void 0 ? void 0 : finalMsg_1.options) === null || _w === void 0 ? void 0 : _w.length) ? finalMsg_1.options : [];
37937
38004
  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) {
37938
38005
  if (index === streamingIndex_4 && isChatMsgDetail(chat)) {
37939
38006
  return __assign$1(__assign$1({}, chat), { msg: finalMsg_1, isStreaming: false, wasStreamed: true, toolCalls: toolCalls_1 });