@xapp/chat-widget 1.90.0 → 1.92.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
@@ -2993,7 +2993,9 @@ function setSessionId(sessionId) {
2993
2993
 
2994
2994
  // Endpoint path constants
2995
2995
  var HOMEOWNER_ENDPOINT_PATH = "/chat/stream";
2996
+ var HOMEOWNER_CLOSE_ENDPOINT_PATH = "/chat/close";
2996
2997
  var BUSINESS_OWNER_ENDPOINT_PATH = "/api/chat/stream";
2998
+ var BUSINESS_OWNER_CLOSE_ENDPOINT_PATH = "/api/chat/close";
2997
2999
  // Default retry configuration for MCP connections
2998
3000
  var DEFAULT_MCP_RETRY_CONFIG = [
2999
3001
  { retry: 0, delay: 0, text: "" }, // Initial attempt (silent)
@@ -3147,6 +3149,22 @@ function clearStoredLastSeq(sessionId) {
3147
3149
  // sessionStorage may be unavailable
3148
3150
  }
3149
3151
  }
3152
+ /**
3153
+ * Returns the current local time as an ISO 8601 string with the local timezone offset.
3154
+ * Unlike toISOString(), this preserves the user's local time zone (e.g. "2024-01-15T14:30:00-04:00")
3155
+ * so the agent server can correctly determine time-of-day context (morning greetings, etc.).
3156
+ */
3157
+ function getLocalISOString(date) {
3158
+ if (date === void 0) { date = new Date(); }
3159
+ var offsetMinutes = -date.getTimezoneOffset();
3160
+ var sign = offsetMinutes >= 0 ? "+" : "-";
3161
+ var pad = function (n) { return String(Math.abs(n)).padStart(2, "0"); };
3162
+ var offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
3163
+ var offsetMins = Math.abs(offsetMinutes) % 60;
3164
+ return ("".concat(date.getFullYear(), "-").concat(pad(date.getMonth() + 1), "-").concat(pad(date.getDate())) +
3165
+ "T".concat(pad(date.getHours()), ":").concat(pad(date.getMinutes()), ":").concat(pad(date.getSeconds())) +
3166
+ "".concat(sign).concat(pad(offsetHours), ":").concat(pad(offsetMins)));
3167
+ }
3150
3168
  /**
3151
3169
  * Gets the current page context from browser globals.
3152
3170
  * If an override is provided, uses that instead (useful for testing).
@@ -3155,7 +3173,7 @@ function clearStoredLastSeq(sessionId) {
3155
3173
  * may contain sensitive data (tokens, PII). Only origin + pathname are sent.
3156
3174
  */
3157
3175
  function getPageContext(override) {
3158
- var localTime = new Date().toISOString();
3176
+ var localTime = getLocalISOString();
3159
3177
  // If override provided, merge with defaults (override wins)
3160
3178
  // Overrides are typically for testing and may include full URLs
3161
3179
  if (override && Object.keys(override).length > 0) {
@@ -3195,9 +3213,11 @@ function getPageContext(override) {
3195
3213
  }
3196
3214
  var MCPChat = /** @class */ (function () {
3197
3215
  function MCPChat(config, options) {
3216
+ var _a;
3198
3217
  this._userId = "";
3199
3218
  this._sessionId = "";
3200
3219
  this._accessToken = "";
3220
+ this._debugMode = false;
3201
3221
  this._attributes = {};
3202
3222
  // Streaming throttle state
3203
3223
  this.streamBuffer = "";
@@ -3207,16 +3227,20 @@ var MCPChat = /** @class */ (function () {
3207
3227
  this.isValidatingSession = false;
3208
3228
  // Tool call tracking for debug mode
3209
3229
  this.currentToolCalls = [];
3210
- // TODO: isReplaying and lastSeq are tracked for future replay support.
3211
- // Wire up replay requests in wakeup() once the server endpoint supports it.
3212
3230
  this.isReplaying = false;
3231
+ // Sleep/wakeup state for tab backgrounding recovery
3232
+ this.isSleeping = false;
3233
+ this.isRequestingReplay = false;
3234
+ this.needsReplayOnWakeup = false;
3213
3235
  // True when userId was assigned by the server (via connected SSE event),
3214
3236
  // meaning it should not be overwritten by the default client fingerprint.
3215
3237
  this.userIdIsServerAssigned = false;
3238
+ this.sentMessageCount = 0;
3216
3239
  this.config = config;
3217
3240
  this.options = options;
3218
3241
  this.configurableMessages = options === null || options === void 0 ? void 0 : options.configurableMessages;
3219
3242
  this.isNewSession = false;
3243
+ this._debugMode = (_a = config.debugMode) !== null && _a !== void 0 ? _a : false;
3220
3244
  this.hooks = options === null || options === void 0 ? void 0 : options.hooks;
3221
3245
  // Validate business_owner mode requires authToken
3222
3246
  var mode = config.mode || "homeowner";
@@ -3248,6 +3272,7 @@ var MCPChat = /** @class */ (function () {
3248
3272
  }
3249
3273
  MCPChat.prototype.init = function (dispatch) {
3250
3274
  this.dispatch = dispatch;
3275
+ this.sessionStartTime = Date.now();
3251
3276
  var mode = this.config.mode || "homeowner";
3252
3277
  log("MCPChat initialized with mode: ".concat(mode));
3253
3278
  // Warn if not using HTTPS with authentication
@@ -3259,14 +3284,8 @@ var MCPChat = /** @class */ (function () {
3259
3284
  // Dispatch bot joined early so it appears before the greeting.
3260
3285
  // getBot() is config-derived (uses options.bot), so it's safe to call before setVisitorInfo.
3261
3286
  this.userJoined(this.getBot(undefined));
3262
- // Returning user with existing session - validate session silently
3263
- // New user without session - initialize new session to get greeting
3264
- if (this._sessionId) {
3265
- this.validateSession();
3266
- }
3267
- else {
3268
- this.initializeNewSession();
3269
- }
3287
+ // Session initialization is deferred to setVisitorInfo() to avoid
3288
+ // making network requests before the widget is visible.
3270
3289
  };
3271
3290
  /**
3272
3291
  * Validates an existing session with the server ("Silent Handshake").
@@ -3320,7 +3339,7 @@ var MCPChat = /** @class */ (function () {
3320
3339
  */
3321
3340
  MCPChat.prototype.buildRequestPayload = function (additionalData) {
3322
3341
  if (additionalData === void 0) { additionalData = {}; }
3323
- return __assign({
3342
+ return __assign(__assign({
3324
3343
  // Identity - server generates these, we just send back what we have
3325
3344
  userId: this._userId || undefined, sessionId: this._sessionId || undefined,
3326
3345
  // Fingerprint for user identification (can be disabled for privacy)
@@ -3328,7 +3347,9 @@ var MCPChat = /** @class */ (function () {
3328
3347
  // Page context
3329
3348
  pageContext: getPageContext(this.config.pageContextOverride),
3330
3349
  // Client timestamp for latency measurement and clock drift detection
3331
- clientTime: new Date().toISOString() }, additionalData);
3350
+ clientTime: new Date().toISOString(),
3351
+ // Last sequence number for replay on reconnect
3352
+ lastSeq: this.lastSeq }, (this._debugMode && { debug: true })), additionalData);
3332
3353
  };
3333
3354
  /**
3334
3355
  * Initializes a new session by calling the endpoint with { newSession: true }.
@@ -3338,9 +3359,12 @@ var MCPChat = /** @class */ (function () {
3338
3359
  return __awaiter$1(this, void 0, void 0, function () {
3339
3360
  var endpoint, error_2;
3340
3361
  var _this = this;
3341
- return __generator$1(this, function (_a) {
3342
- switch (_a.label) {
3362
+ var _a;
3363
+ return __generator$1(this, function (_b) {
3364
+ switch (_b.label) {
3343
3365
  case 0:
3366
+ // Reset debug mode on new session (respects config default)
3367
+ this._debugMode = (_a = this.config.debugMode) !== null && _a !== void 0 ? _a : false;
3344
3368
  // Prevent multiple concurrent initialization requests
3345
3369
  if (this.isInitializingSession) {
3346
3370
  log("New session initialization already in progress, skipping");
@@ -3354,15 +3378,15 @@ var MCPChat = /** @class */ (function () {
3354
3378
  _this.resolveSessionInit = resolve;
3355
3379
  });
3356
3380
  this.typing();
3357
- _a.label = 1;
3381
+ _b.label = 1;
3358
3382
  case 1:
3359
- _a.trys.push([1, 3, 4, 5]);
3383
+ _b.trys.push([1, 3, 4, 5]);
3360
3384
  return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload({ newSession: true, generateGreeting: true }))];
3361
3385
  case 2:
3362
- _a.sent();
3386
+ _b.sent();
3363
3387
  return [3 /*break*/, 5];
3364
3388
  case 3:
3365
- error_2 = _a.sent();
3389
+ error_2 = _b.sent();
3366
3390
  err("Failed to initialize new session: ".concat(error_2));
3367
3391
  this.stopTyping();
3368
3392
  this.setConnectionStatus("offline");
@@ -3401,6 +3425,14 @@ var MCPChat = /** @class */ (function () {
3401
3425
  // homeowner mode - serverUrl should already include appId
3402
3426
  return "".concat(serverUrl).concat(HOMEOWNER_ENDPOINT_PATH);
3403
3427
  };
3428
+ MCPChat.prototype.getCloseEndpoint = function () {
3429
+ var serverUrl = this.config.serverUrl.replace(/\/+$/, "");
3430
+ var mode = this.config.mode || "homeowner";
3431
+ if (mode === "business_owner") {
3432
+ return "".concat(serverUrl).concat(BUSINESS_OWNER_CLOSE_ENDPOINT_PATH);
3433
+ }
3434
+ return "".concat(serverUrl).concat(HOMEOWNER_CLOSE_ENDPOINT_PATH);
3435
+ };
3404
3436
  MCPChat.prototype.sendNewMessage = function (msg, user) {
3405
3437
  log("SERVER: new message: ".concat(JSON.stringify(msg)));
3406
3438
  this.dispatch({
@@ -3551,6 +3583,30 @@ var MCPChat = /** @class */ (function () {
3551
3583
  },
3552
3584
  });
3553
3585
  };
3586
+ MCPChat.prototype.dispatchStreamAbort = function (messageId) {
3587
+ log("[dispatchStreamAbort] Stream aborted: ".concat(messageId));
3588
+ this.dispatch({
3589
+ type: "chat",
3590
+ detail: {
3591
+ type: "chat.stream.abort",
3592
+ user: this.getBot(undefined),
3593
+ messageId: messageId,
3594
+ timestamp: Date.now(),
3595
+ },
3596
+ });
3597
+ };
3598
+ MCPChat.prototype.dispatchStreamResume = function (messageId) {
3599
+ log("[dispatchStreamResume] Stream resumed: ".concat(messageId));
3600
+ this.dispatch({
3601
+ type: "chat",
3602
+ detail: {
3603
+ type: "chat.stream.resume",
3604
+ user: this.getBot(undefined),
3605
+ messageId: messageId,
3606
+ timestamp: Date.now(),
3607
+ },
3608
+ });
3609
+ };
3554
3610
  MCPChat.prototype.dispatchToolStart = function (params) {
3555
3611
  this.dispatch({
3556
3612
  type: "chat",
@@ -3864,7 +3920,10 @@ var MCPChat = /** @class */ (function () {
3864
3920
  attempt++;
3865
3921
  return [3 /*break*/, 3];
3866
3922
  case 14:
3867
- if (!success) {
3923
+ if (success) {
3924
+ this.sentMessageCount++;
3925
+ }
3926
+ else {
3868
3927
  log("All MCP retry attempts exhausted");
3869
3928
  }
3870
3929
  return [2 /*return*/];
@@ -3877,10 +3936,28 @@ var MCPChat = /** @class */ (function () {
3877
3936
  */
3878
3937
  MCPChat.prototype.attemptSSERequest = function (message) {
3879
3938
  return __awaiter$1(this, void 0, void 0, function () {
3880
- var messageText;
3939
+ var messageText, statusMsg, msgId;
3881
3940
  var _a;
3882
3941
  return __generator$1(this, function (_b) {
3883
3942
  messageText = ((_a = message.msg) === null || _a === void 0 ? void 0 : _a.text) || "";
3943
+ // Intercept /debug command — toggle debug mode client-side without sending to LLM
3944
+ if (messageText.trim().toLowerCase() === '/debug') {
3945
+ this._debugMode = !this._debugMode;
3946
+ statusMsg = this._debugMode ? 'Debug mode enabled' : 'Debug mode disabled';
3947
+ log("[attemptSSERequest] ".concat(statusMsg));
3948
+ msgId = "debug-".concat(Date.now());
3949
+ this.dispatch({
3950
+ type: "chat",
3951
+ detail: {
3952
+ type: "chat.stream.end",
3953
+ user: this.getBot(undefined),
3954
+ messageId: msgId,
3955
+ msg: { text: statusMsg },
3956
+ timestamp: +new Date(),
3957
+ },
3958
+ });
3959
+ return [2 /*return*/];
3960
+ }
3884
3961
  log("[attemptSSERequest] Sending message: \"".concat(messageText, "\", userId: ").concat(this._userId, ", sessionId: ").concat(this._sessionId));
3885
3962
  return [2 /*return*/, this.performSSERequest(this.buildRequestPayload({ message: messageText }))];
3886
3963
  });
@@ -3916,6 +3993,10 @@ var MCPChat = /** @class */ (function () {
3916
3993
  : {})),
3917
3994
  body: JSON.stringify(body),
3918
3995
  signal: _this.abortController.signal,
3996
+ // Keep the connection open when the tab is hidden. Without this,
3997
+ // fetchEventSource closes and re-POSTs on visibility change, which
3998
+ // creates duplicate streaming messages and out-of-order responses.
3999
+ openWhenHidden: true,
3919
4000
  onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
3920
4001
  var error;
3921
4002
  return __generator$1(this, function (_a) {
@@ -3932,6 +4013,11 @@ var MCPChat = /** @class */ (function () {
3932
4013
  });
3933
4014
  }); },
3934
4015
  onmessage: function (event) {
4016
+ // Guard: ignore events from stale/aborted requests
4017
+ if (_this.currentMessageId !== messageId) {
4018
+ log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
4019
+ return;
4020
+ }
3935
4021
  log("SSE event: ".concat(event.event, ", data: ").concat(event.data));
3936
4022
  var eventType = event.event || "message";
3937
4023
  var data = {};
@@ -3971,11 +4057,17 @@ var MCPChat = /** @class */ (function () {
3971
4057
  storeIdentity(data.userId, data.sessionId);
3972
4058
  log("[SSE] Identity from connected - userId: ".concat(data.userId, ", sessionId: ").concat(data.sessionId));
3973
4059
  }
3974
- // Always start streaming - server sends greeting for both new and returning users
3975
- // (resumeSession requests always get a personalized greeting)
3976
4060
  _this.currentToolCalls = [];
3977
4061
  _this.stopTyping();
3978
- _this.dispatchStreamStart(messageId);
4062
+ if (data.isReplay) {
4063
+ // Replay connection — resume into the existing partial
4064
+ // message instead of creating a duplicate placeholder
4065
+ _this.dispatchStreamResume(messageId);
4066
+ }
4067
+ else {
4068
+ // Normal connection — create new streaming placeholder
4069
+ _this.dispatchStreamStart(messageId);
4070
+ }
3979
4071
  _this.startStreamThrottle(messageId);
3980
4072
  break;
3981
4073
  case "session_expired":
@@ -3998,9 +4090,9 @@ var MCPChat = /** @class */ (function () {
3998
4090
  if (data.text) {
3999
4091
  log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
4000
4092
  textChunks.push(data.text);
4001
- if (data.isReplay) {
4002
- // Replayed text — dispatch immediately (no throttle)
4003
- // so the user sees caught-up content instantly
4093
+ if (data.isReplay || _this.isSleeping) {
4094
+ // Replayed text or tab hidden — dispatch immediately
4095
+ // (no throttle timer running in either case)
4004
4096
  _this.dispatchStreamChunk(messageId, data.text);
4005
4097
  }
4006
4098
  else {
@@ -4050,8 +4142,19 @@ var MCPChat = /** @class */ (function () {
4050
4142
  toolCall.error = data.toolError;
4051
4143
  toolCall.label = data.label; // Update with completion label
4052
4144
  toolCall.hidden = data.hidden; // Update hidden status
4053
- toolCall.endTime = Date.now();
4145
+ // Use server-side batch duration if available (debug mode), otherwise client-side
4146
+ toolCall.endTime = (data.toolBatchDurationMs && toolCall.startTime != null)
4147
+ ? toolCall.startTime + data.toolBatchDurationMs
4148
+ : Date.now();
4054
4149
  toolCall.isExecuting = false;
4150
+ // Capture tool input from debug-enriched events
4151
+ if (data.toolInput) {
4152
+ toolCall.input = data.toolInput;
4153
+ }
4154
+ }
4155
+ // Toggle debug mode when toggle_debug tool is called by the LLM
4156
+ if (data.toolName === 'toggle_debug') {
4157
+ _this._debugMode = !_this._debugMode;
4055
4158
  }
4056
4159
  // Always dispatch tool end for user-friendly progress display
4057
4160
  _this.dispatchToolEnd({
@@ -4099,8 +4202,8 @@ var MCPChat = /** @class */ (function () {
4099
4202
  sources: sources,
4100
4203
  displays: data.response.displays,
4101
4204
  };
4102
- // Capture tool calls before flushing (debug mode)
4103
- var toolCalls_1 = _this.config.debugMode ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
4205
+ // Capture tool calls before flushing (debug mode — config or runtime toggle)
4206
+ var toolCalls_1 = (_this.config.debugMode || _this._debugMode) ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
4104
4207
  _this.flushStreamBuffer().then(function () {
4105
4208
  _this.dispatchStreamEnd(messageId, finalMessage_1, toolCalls_1);
4106
4209
  _this.currentToolCalls = []; // Clear after dispatch
@@ -4135,7 +4238,20 @@ var MCPChat = /** @class */ (function () {
4135
4238
  if (_this.currentMessageId === messageId && !hasError) {
4136
4239
  hasError = true;
4137
4240
  _this.isReplaying = false;
4138
- _this.stopStreamThrottle();
4241
+ if (_this.isSleeping) {
4242
+ // Connection dropped while tab is hidden — don't treat as
4243
+ // normal error. Finalize partial message and request replay
4244
+ // when the user returns.
4245
+ log("[MCPChat] SSE error during sleep - will request replay on wakeup");
4246
+ _this.needsReplayOnWakeup = true;
4247
+ if (_this.streamingMessageIdForThrottle) {
4248
+ _this.dispatchStreamAbort(messageId);
4249
+ }
4250
+ _this.stopStreamThrottle();
4251
+ }
4252
+ else {
4253
+ _this.stopStreamThrottle();
4254
+ }
4139
4255
  sseError = error instanceof Error ? error : new Error(String(error));
4140
4256
  // Get status code if available
4141
4257
  var statusCode = (error === null || error === void 0 ? void 0 : error.statusCode) || httpStatusCode;
@@ -4182,12 +4298,20 @@ var MCPChat = /** @class */ (function () {
4182
4298
  this._attributes.currentUrl = window.location.href;
4183
4299
  }
4184
4300
  this._accessToken = this.visitorInfo.accessToken;
4185
- // Bot joined is dispatched in init() to ensure correct ordering
4186
- // Show typing indicator if we're still waiting for initial greeting
4301
+ this.startSession(sessionId);
4302
+ // Initialize session now that the widget is visible and visitor info is set.
4303
+ // Returning user with existing session - validate session silently
4304
+ // New user without session - initialize new session to get greeting
4305
+ if (this._sessionId) {
4306
+ this.validateSession();
4307
+ }
4308
+ else {
4309
+ this.initializeNewSession();
4310
+ }
4311
+ // Show typing indicator while waiting for session init response
4187
4312
  if (this.isInitializingSession) {
4188
4313
  this.typing();
4189
4314
  }
4190
- this.startSession(sessionId);
4191
4315
  cb();
4192
4316
  };
4193
4317
  MCPChat.prototype.sendChatRating = function () { };
@@ -4204,23 +4328,138 @@ var MCPChat = /** @class */ (function () {
4204
4328
  }
4205
4329
  };
4206
4330
  MCPChat.prototype.sleep = function () {
4207
- this.stopStreamThrottle();
4208
- this.isReplaying = false;
4209
- if (this.abortController) {
4210
- this.abortController.abort();
4211
- this.abortController = undefined;
4331
+ this.isSleeping = true;
4332
+ if (this.streamingMessageIdForThrottle) {
4333
+ // Active stream in progress — keep SSE connection alive so the
4334
+ // response completes naturally while the tab is hidden.
4335
+ this.needsReplayOnWakeup = false;
4336
+ log("[MCPChat] sleep() - stream in progress, keeping SSE connection alive");
4337
+ // Flush any buffered text to Redux immediately
4338
+ if (this.streamBuffer.length > 0) {
4339
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4340
+ this.streamBuffer = "";
4341
+ }
4342
+ // Stop the visual throttle timer (browsers throttle timers anyway)
4343
+ // but keep streamingMessageIdForThrottle so incoming chunks can be dispatched
4344
+ if (this.streamThrottleTimer) {
4345
+ clearInterval(this.streamThrottleTimer);
4346
+ this.streamThrottleTimer = undefined;
4347
+ }
4348
+ if (this.visibilityHandler) {
4349
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
4350
+ this.visibilityHandler = undefined;
4351
+ }
4352
+ }
4353
+ else {
4354
+ // No active stream — safe to abort the idle connection
4355
+ this.stopStreamThrottle();
4356
+ this.isReplaying = false;
4357
+ if (this.abortController) {
4358
+ this.abortController.abort();
4359
+ this.abortController = undefined;
4360
+ }
4212
4361
  }
4213
4362
  };
4214
4363
  MCPChat.prototype.wakeup = function () {
4215
- // When tab is foregrounded, just restore connection status.
4216
- // Don't validate/replay yet — the session will be validated
4217
- // on the next user message. This avoids creating duplicate
4218
- // greetings if validation fails transiently.
4364
+ this.isSleeping = false;
4365
+ if (this.streamingMessageIdForThrottle && this.abortController) {
4366
+ // Stream is still in progress — restart the visual throttle
4367
+ log("[MCPChat] wakeup() - stream still in progress, restarting throttle");
4368
+ // Flush accumulated buffer before startStreamThrottle (which resets it)
4369
+ if (this.streamBuffer.length > 0) {
4370
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4371
+ this.streamBuffer = "";
4372
+ }
4373
+ this.startStreamThrottle(this.streamingMessageIdForThrottle);
4374
+ }
4375
+ else if (this.needsReplayOnWakeup) {
4376
+ this.needsReplayOnWakeup = false;
4377
+ if (this.lastSeq !== undefined && this._sessionId) {
4378
+ // Connection dropped during sleep — request replay of missed events.
4379
+ // Don't set "online" yet — requestReplay is fire-and-forget and the
4380
+ // connection status will be updated by the SSE onopen handler.
4381
+ log("[MCPChat] wakeup() - connection lost during sleep, requesting replay");
4382
+ this.requestReplay();
4383
+ return;
4384
+ }
4385
+ // Cannot replay — missing lastSeq or sessionId
4386
+ err("[MCPChat] wakeup() - cannot replay, missing lastSeq (".concat(this.lastSeq, ") or sessionId (").concat(this._sessionId, ")"));
4387
+ }
4219
4388
  if (this._sessionId) {
4220
4389
  log("[MCPChat] Wakeup - session exists, lastSeq: ".concat(this.lastSeq));
4221
4390
  this.setConnectionStatus("online");
4222
4391
  }
4223
4392
  };
4393
+ MCPChat.prototype.requestReplay = function () {
4394
+ return __awaiter$1(this, void 0, void 0, function () {
4395
+ var error_5;
4396
+ return __generator$1(this, function (_a) {
4397
+ switch (_a.label) {
4398
+ case 0:
4399
+ if (this.isRequestingReplay) {
4400
+ log("[MCPChat] Replay already in progress, skipping");
4401
+ return [2 /*return*/];
4402
+ }
4403
+ this.isRequestingReplay = true;
4404
+ this.typing();
4405
+ _a.label = 1;
4406
+ case 1:
4407
+ _a.trys.push([1, 3, 4, 5]);
4408
+ // lastSeq is already included by buildRequestPayload
4409
+ return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload())];
4410
+ case 2:
4411
+ // lastSeq is already included by buildRequestPayload
4412
+ _a.sent();
4413
+ log("[MCPChat] Replay request completed");
4414
+ return [3 /*break*/, 5];
4415
+ case 3:
4416
+ error_5 = _a.sent();
4417
+ err("[MCPChat] Replay request failed: ".concat(error_5));
4418
+ this.setConnectionStatus("offline");
4419
+ return [3 /*break*/, 5];
4420
+ case 4:
4421
+ this.stopTyping();
4422
+ this.isRequestingReplay = false;
4423
+ return [7 /*endfinally*/];
4424
+ case 5: return [2 /*return*/];
4425
+ }
4426
+ });
4427
+ });
4428
+ };
4429
+ /**
4430
+ * Sends a best-effort close notification to the agent server.
4431
+ * Uses navigator.sendBeacon() so the request survives page unload.
4432
+ * Called from the beforeunload event handler and the widget close button.
4433
+ */
4434
+ MCPChat.prototype.notifyDisconnecting = function (reason) {
4435
+ if (reason === void 0) { reason = "close_tab"; }
4436
+ if (!this._sessionId) {
4437
+ return;
4438
+ }
4439
+ var endpoint = this.getCloseEndpoint();
4440
+ var payload = JSON.stringify({
4441
+ sessionId: this._sessionId,
4442
+ userId: this._userId || undefined,
4443
+ reason: reason,
4444
+ messageCount: this.sentMessageCount,
4445
+ durationMs: this.sessionStartTime !== undefined ? Date.now() - this.sessionStartTime : 0,
4446
+ });
4447
+ log("[MCPChat] notifyDisconnecting: reason=".concat(reason, ", endpoint=").concat(endpoint));
4448
+ // For widget_close, the page is not unloading so fetch() can be used — it supports
4449
+ // auth headers (needed for business_owner mode) unlike sendBeacon.
4450
+ if (reason === "widget_close" && typeof fetch !== "undefined") {
4451
+ var headers = { "Content-Type": "application/json" };
4452
+ if (this.config.authToken) {
4453
+ headers["Authorization"] = "Bearer ".concat(this.config.authToken);
4454
+ }
4455
+ fetch(endpoint, { method: "POST", headers: headers, body: payload, keepalive: true });
4456
+ return;
4457
+ }
4458
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
4459
+ var blob = new Blob([payload], { type: "application/json" });
4460
+ navigator.sendBeacon(endpoint, blob);
4461
+ }
4462
+ };
4224
4463
  MCPChat.prototype.startSession = function (sessionId) {
4225
4464
  // userId priority:
4226
4465
  // 1. Existing server-assigned userId (from localStorage or previous connected event)
@@ -10795,6 +11034,14 @@ function isCard(display) {
10795
11034
  function isList(display) {
10796
11035
  return !!display && (display.type === "LIST" || display.type === "CAROUSEL");
10797
11036
  }
11037
+ /**
11038
+ * Is the Display a Table
11039
+ */
11040
+ function isTable(display) {
11041
+ return !!display && display.type === "TABLE"
11042
+ && Array.isArray(display.headers)
11043
+ && Array.isArray(display.rows);
11044
+ }
10798
11045
 
10799
11046
  var CardMiddlewareWidget = function (props) {
10800
11047
  var msg = props.msg, ctx = props.ctx;
@@ -11095,10 +11342,31 @@ var MultiSelectMiddleware = function (next) { return function (props) {
11095
11342
  return next(props);
11096
11343
  }; };
11097
11344
 
11345
+ var Table = function (_a) {
11346
+ var table = _a.table;
11347
+ return (require$$0.jsxs("div", { className: "xappw-table", children: [table.title && require$$0.jsx("div", { className: "xappw-table__header", children: table.title }), require$$0.jsx("div", { className: "xappw-table__container", children: require$$0.jsxs("table", { children: [require$$0.jsx("thead", { children: require$$0.jsx("tr", { children: table.headers.map(function (header, i) { return (require$$0.jsx("th", { scope: "col", children: header }, "".concat(header, "-").concat(i))); }) }) }), require$$0.jsx("tbody", { children: table.rows.map(function (row, rowIndex) { return (require$$0.jsx("tr", { children: row.map(function (cell, cellIndex) { return (require$$0.jsx("td", { children: cell }, cellIndex)); }) }, rowIndex)); }) })] }) })] }));
11348
+ };
11349
+
11350
+ var TableMiddlewareWidget = function (props) {
11351
+ var msg = props.msg, ctx = props.ctx;
11352
+ if (!ctx) {
11353
+ throw new TypeError("ctx not provided to ".concat(TableMiddlewareWidget.name));
11354
+ }
11355
+ return (require$$0.jsx(ChatMessagePart, { showAvatar: false, user: ctx.user, fullWidth: true, children: require$$0.jsx("div", { className: "chat-msg chat-msg--expand", children: require$$0.jsx(Table, { table: msg }) }) }));
11356
+ };
11357
+ var TableMiddleware = function (next) { return function (props) {
11358
+ var msg = props.msg, ctx = props.ctx;
11359
+ if (isTable(msg)) {
11360
+ return require$$0.jsx(TableMiddlewareWidget, { msg: msg, ctx: ctx });
11361
+ }
11362
+ return next(props);
11363
+ }; };
11364
+
11098
11365
  var StandardMiddlewaresBuilder = joinMiddlewares([
11099
11366
  ListMiddleware,
11100
11367
  CardMiddleware,
11101
- MultiSelectMiddleware
11368
+ MultiSelectMiddleware,
11369
+ TableMiddleware
11102
11370
  ]);
11103
11371
  function useStandardMiddlewareBuilder() {
11104
11372
  var lateMiddleware = useLateMiddleware();
@@ -11352,14 +11620,58 @@ var ToolCallDisplay = function (_a) {
11352
11620
  };
11353
11621
 
11354
11622
  /**
11355
- * Parses basic markdown to HTML.
11356
- * Supports: bold, italic, links, headers, and unordered lists.
11623
+ * Escape &, <, > for safe insertion into HTML text nodes.
11624
+ * Not suitable for attribute values.
11357
11625
  */
11626
+ function escapeHtml(text) {
11627
+ return text
11628
+ .replace(/&/g, "&amp;")
11629
+ .replace(/</g, "&lt;")
11630
+ .replace(/>/g, "&gt;");
11631
+ }
11632
+ function parseInlineMarkdown(text) {
11633
+ return escapeHtml(text)
11634
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
11635
+ .replace(/__(.+?)__/g, "<strong>$1</strong>")
11636
+ .replace(/\*([^*]+?)\*/g, "<em>$1</em>")
11637
+ .replace(/_([^_]+?)_/g, "<em>$1</em>");
11638
+ }
11639
+ function tableToHtml(headerLine, separatorLine, bodyBlock) {
11640
+ var parseRow = function (row) {
11641
+ return row.split("|").slice(1, -1).map(function (cell) { return parseInlineMarkdown(cell.trim()); });
11642
+ };
11643
+ var separators = separatorLine.split("|").slice(1, -1).map(function (s) { return s.trim(); });
11644
+ var alignments = separators.map(function (sep) {
11645
+ if (sep.startsWith(":") && sep.endsWith(":"))
11646
+ return "center";
11647
+ if (sep.endsWith(":"))
11648
+ return "right";
11649
+ return "left";
11650
+ });
11651
+ var headers = parseRow(headerLine);
11652
+ var headerHtml = headers.map(function (h, i) {
11653
+ return "<th scope=\"col\" style=\"text-align:".concat(alignments[i] || "left", "\">").concat(h, "</th>");
11654
+ }).join("");
11655
+ var bodyRows = bodyBlock.trim().split("\n").filter(function (row) { return row.trim() !== ""; });
11656
+ var bodyHtml = bodyRows.map(function (row) {
11657
+ var cells = parseRow(row);
11658
+ return "<tr>" + cells.map(function (c, i) {
11659
+ return "<td style=\"text-align:".concat(alignments[i] || "left", "\">").concat(c, "</td>");
11660
+ }).join("") + "</tr>";
11661
+ }).join("");
11662
+ return "<div style=\"overflow-x:auto\"><table><thead><tr>".concat(headerHtml, "</tr></thead><tbody>").concat(bodyHtml, "</tbody></table></div>");
11663
+ }
11358
11664
  function parseMarkdown(text) {
11359
11665
  if (!text)
11360
11666
  return "";
11361
- var html = text
11362
- // Escape HTML entities first
11667
+ // Extract tables first, replace with placeholders, then process the rest
11668
+ var tables = [];
11669
+ var processed = text.replace(/(?:^|\n)(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/g, function (_match, headerLine, separatorLine, bodyBlock) {
11670
+ tables.push(tableToHtml(headerLine, separatorLine, bodyBlock));
11671
+ return "\n%%TABLE".concat(tables.length - 1, "%%\n");
11672
+ });
11673
+ // Now safely escape and parse markdown on the non-table text
11674
+ processed = processed
11363
11675
  .replace(/&/g, "&amp;")
11364
11676
  .replace(/</g, "&lt;")
11365
11677
  .replace(/>/g, "&gt;")
@@ -11380,13 +11692,17 @@ function parseMarkdown(text) {
11380
11692
  // Line breaks
11381
11693
  .replace(/\n/g, "<br>");
11382
11694
  // Wrap consecutive <li> elements in <ul>
11383
- html = html.replace(/(<li>.*?<\/li>)(<br>)?/g, "$1");
11384
- html = html.replace(/(<li>.*?<\/li>)+/g, "<ul>$&</ul>");
11695
+ processed = processed.replace(/(<li>.*?<\/li>)(<br>)?/g, "$1");
11696
+ processed = processed.replace(/(<li>.*?<\/li>)+/g, "<ul>$&</ul>");
11385
11697
  // Clean up <br> inside <ul>
11386
- html = html.replace(/<ul>(.*?)<\/ul>/g, function (match) {
11698
+ processed = processed.replace(/<ul>(.*?)<\/ul>/g, function (match) {
11387
11699
  return match.replace(/<br>/g, "");
11388
11700
  });
11389
- return html;
11701
+ // Restore table placeholders (strip adjacent <br> tags)
11702
+ tables.forEach(function (tableHtml, i) {
11703
+ processed = processed.replace(new RegExp("(?:<br>)?%%TABLE".concat(i, "%%(?:<br>)?")), tableHtml);
11704
+ });
11705
+ return processed;
11390
11706
  }
11391
11707
  var ChatTextMessage = function (props) {
11392
11708
  var _a;
@@ -35454,7 +35770,7 @@ var ChatWidgetConnected = function (props) {
35454
35770
  * Exported for use by StaticChatWidgetContainer to avoid infinite loops.
35455
35771
  */
35456
35772
  var ChatWidgetUI = function (props) {
35457
- 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;
35773
+ 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;
35458
35774
  var innerDispatch = useChatDispatch();
35459
35775
  var dispatch = useChatServerDispatch();
35460
35776
  // From Redux
@@ -35484,24 +35800,30 @@ var ChatWidgetUI = function (props) {
35484
35800
  chatState.visuals = {};
35485
35801
  }
35486
35802
  // Our state - pull from storage
35487
- var _15 = require$$1.useState((!canMinimize && !canCancel) ||
35803
+ // Parse autoOpenOnWidth once for use in both initial state and useEffect
35804
+ var autoOpenOnWidthValue = ((_m = props.config) === null || _m === void 0 ? void 0 : _m.autoOpenOnWidth)
35805
+ ? parseInt(props.config.autoOpenOnWidth, 10)
35806
+ : NaN;
35807
+ var autoOpenOnWidthMatch = !isNaN(autoOpenOnWidthValue) && window.innerWidth >= autoOpenOnWidthValue;
35808
+ // When autoOpenOnWidth is configured, use width check instead of persisted visible state
35809
+ // to prevent greeting requests on viewports below the threshold
35810
+ var _14 = require$$1.useState((!canMinimize && !canCancel) ||
35488
35811
  // !!get("visible") ||
35489
- chatState.visuals.visible ||
35490
- (((_m = props.config) === null || _m === void 0 ? void 0 : _m.autoOpenOnWidth) &&
35491
- window.matchMedia("(min-width: ".concat((_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnWidth, ")")).matches)), visible = _15[0], setVisibleState = _15[1];
35492
- var _16 = require$$1.useState(false); _16[0]; var setTypingState = _16[1]; // false initially - state kept for potential external observers
35812
+ (!isNaN(autoOpenOnWidthValue) ? autoOpenOnWidthMatch : chatState.visuals.visible)), visible = _14[0], setVisibleState = _14[1];
35813
+ var _15 = require$$1.useState(false); _15[0]; var setTypingState = _15[1]; // false initially - state kept for potential external observers
35493
35814
  // Ref to track typing state for use in timeout callbacks
35494
35815
  var typingRef = require$$1.useRef(false);
35495
35816
  // Timeout ref for debouncing "stop typing" events
35496
35817
  var stopTypingTimeoutRef = require$$1.useRef(null);
35818
+ // Track whether autoOpenOnWidth has already fired so it doesn't re-open after user closes
35819
+ var autoOpenFiredRef = require$$1.useRef(autoOpenOnWidthMatch);
35497
35820
  var chatServer = require$$1.useContext(ChatServerContext);
35498
- var patternsConfig = (_q = (_p = props.config) === null || _p === void 0 ? void 0 : _p.autoOpenOnPattern) === null || _q === void 0 ? void 0 : _q.patterns;
35821
+ var patternsConfig = (_p = (_o = props.config) === null || _o === void 0 ? void 0 : _o.autoOpenOnPattern) === null || _p === void 0 ? void 0 : _p.patterns;
35499
35822
  var currentUrl = window.location.href;
35500
35823
  var patternExist = patternsConfig && patternsConfig.length > 0;
35501
35824
  var patternMatches = patternsConfig === null || patternsConfig === void 0 ? void 0 : patternsConfig.some(function (pattern) { return currentUrl.includes(pattern); });
35502
- var configWidth = (_s = (_r = props.config) === null || _r === void 0 ? void 0 : _r.autoOpenOnPattern) === null || _s === void 0 ? void 0 : _s.minimumWidth;
35503
- // eslint-disable-next-line no-restricted-globals
35504
- var currentWidth = screen.width;
35825
+ var configWidth = (_r = (_q = props.config) === null || _q === void 0 ? void 0 : _q.autoOpenOnPattern) === null || _r === void 0 ? void 0 : _r.minimumWidth;
35826
+ var currentWidth = window.innerWidth;
35505
35827
  var setVisible = require$$1.useCallback(function (newVisible) {
35506
35828
  if (staticMode || previewMode) {
35507
35829
  return;
@@ -35535,7 +35857,7 @@ var ChatWidgetUI = function (props) {
35535
35857
  };
35536
35858
  // eslint-disable-next-line react-hooks/exhaustive-deps
35537
35859
  }, []);
35538
- var _17 = require$$1.useState(!document.hidden), isTabVisible = _17[0], setIsTabVisible = _17[1];
35860
+ var _16 = require$$1.useState(!document.hidden), isTabVisible = _16[0], setIsTabVisible = _16[1];
35539
35861
  require$$1.useEffect(function () {
35540
35862
  var handleVisibilityChange = function () {
35541
35863
  setIsTabVisible(!document.hidden);
@@ -35568,7 +35890,7 @@ var ChatWidgetUI = function (props) {
35568
35890
  var handleBeforeUnload = function () {
35569
35891
  log("beforeunload - notifying router of user disconnecting");
35570
35892
  if (chatServer.notifyDisconnecting) {
35571
- chatServer.notifyDisconnecting();
35893
+ chatServer.notifyDisconnecting("close_tab");
35572
35894
  }
35573
35895
  };
35574
35896
  window.addEventListener("beforeunload", handleBeforeUnload);
@@ -35584,10 +35906,8 @@ var ChatWidgetUI = function (props) {
35584
35906
  if (chatState.visuals.opened) {
35585
35907
  setVisible(true);
35586
35908
  }
35587
- else {
35588
- if (mode === "normal") {
35589
- setVisible(false);
35590
- }
35909
+ else if (mode === "normal" && !autoOpenOnWidthMatch) {
35910
+ setVisible(false);
35591
35911
  }
35592
35912
  if (patternExist && patternMatches) {
35593
35913
  setVisible(true);
@@ -35595,6 +35915,10 @@ var ChatWidgetUI = function (props) {
35595
35915
  if (currentWidth < +configWidth) {
35596
35916
  setVisible(false);
35597
35917
  }
35918
+ if (autoOpenOnWidthMatch && !autoOpenFiredRef.current) {
35919
+ autoOpenFiredRef.current = true;
35920
+ setVisible(true);
35921
+ }
35598
35922
  }, [
35599
35923
  currentWidth,
35600
35924
  patternExist,
@@ -35603,6 +35927,7 @@ var ChatWidgetUI = function (props) {
35603
35927
  setVisible,
35604
35928
  chatState.visuals.opened,
35605
35929
  mode,
35930
+ autoOpenOnWidthMatch,
35606
35931
  ]);
35607
35932
  var stopTyping = require$$1.useCallback(function () {
35608
35933
  if (!typingRef.current)
@@ -35722,6 +36047,9 @@ var ChatWidgetUI = function (props) {
35722
36047
  }
35723
36048
  /** Called when cancel is clicked */
35724
36049
  function handleCancelClick() {
36050
+ if (chatServer === null || chatServer === void 0 ? void 0 : chatServer.notifyDisconnecting) {
36051
+ chatServer.notifyDisconnecting("widget_close");
36052
+ }
35725
36053
  // First reset to clear all state
35726
36054
  innerDispatch(reset());
35727
36055
  // Then set hasInteracted to prevent CTA from showing after cancel
@@ -35747,8 +36075,8 @@ var ChatWidgetUI = function (props) {
35747
36075
  }));
35748
36076
  }
35749
36077
  // Action Bar state and handlers
35750
- var _18 = require$$1.useState(false), formModalOpen = _18[0], setFormModalOpen = _18[1];
35751
- var _19 = require$$1.useState(null), activeFormConfig = _19[0], setActiveFormConfig = _19[1];
36078
+ var _17 = require$$1.useState(false), formModalOpen = _17[0], setFormModalOpen = _17[1];
36079
+ var _18 = require$$1.useState(null), activeFormConfig = _18[0], setActiveFormConfig = _18[1];
35752
36080
  var handleFormButtonClick = require$$1.useCallback(function (formButton) {
35753
36081
  // If form-widget is configured, use its openForm API
35754
36082
  // The form-widget script should already be pre-loaded
@@ -35815,7 +36143,7 @@ var ChatWidgetUI = function (props) {
35815
36143
  var messages = chatState && chatState.chats;
35816
36144
  log("Rendering - accountStatus: \"".concat(chatState.accountStatus, "\", connectionStatus: \"").concat(chatState.connection.connectionStatus, "\""));
35817
36145
  var config = props.config, onConnectionStatusChange = props.onConnectionStatusChange;
35818
- var actionBarEnabled = ((_t = config === null || config === void 0 ? void 0 : config.actionBar) === null || _t === void 0 ? void 0 : _t.enabled) === true;
36146
+ var actionBarEnabled = ((_s = config === null || config === void 0 ? void 0 : config.actionBar) === null || _s === void 0 ? void 0 : _s.enabled) === true;
35819
36147
  // Disable greeting in preview mode
35820
36148
  useGreeting(!isOffline && !props.preChatFormEnabled && visible && !previewMode);
35821
36149
  var connectionStatus = chatState.connection.connectionStatus;
@@ -35824,7 +36152,7 @@ var ChatWidgetUI = function (props) {
35824
36152
  onConnectionStatusChange(connectionStatus);
35825
36153
  }
35826
36154
  }, [connectionStatus, onConnectionStatusChange]);
35827
- useExternalScript((_u = props.config) === null || _u === void 0 ? void 0 : _u.middlewareUrl);
36155
+ useExternalScript((_t = props.config) === null || _t === void 0 ? void 0 : _t.middlewareUrl);
35828
36156
  // Pre-load form-widget script if formWidgetKey is provided
35829
36157
  require$$1.useEffect(function () {
35830
36158
  if (config === null || config === void 0 ? void 0 : config.formWidgetKey) {
@@ -35856,15 +36184,15 @@ var ChatWidgetUI = function (props) {
35856
36184
  };
35857
36185
  }, [actionBarEnabled]);
35858
36186
  // This is a pseudo agent. It represent's the widget (shown in the header avatar for instance)
35859
- var widgetAgent = ((_v = chatState.agents["agent:robot"]) === null || _v === void 0 ? void 0 : _v.user) ||
36187
+ var widgetAgent = ((_u = chatState.agents["agent:robot"]) === null || _u === void 0 ? void 0 : _u.user) ||
35860
36188
  (config === null || config === void 0 ? void 0 : config.agent) || {
35861
36189
  nick: "agent:robot",
35862
36190
  avatarPath: config.avatarUrl,
35863
36191
  display_name: "Agent",
35864
36192
  };
35865
- return (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarEnabled ? " widget-container--with-action-bar" : ""), children: [require$$0.jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), require$$0.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 }), require$$0.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 }), require$$0.jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: require$$0.jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && require$$0.jsx(ServerOffline, {}), chatState.wsButton && visible && (require$$0.jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), require$$0.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 }), require$$0.jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: require$$0.jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarEnabled && config.actionBar ? (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsx(ActionBar, { config: __assign(__assign({}, config.actionBar), {
36193
+ return (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsxs("div", { className: "widget-container ".concat(modeClass, " ").concat(getVisibilityClass()).concat(actionBarEnabled ? " widget-container--with-action-bar" : ""), children: [require$$0.jsx(WidgetStylesheet, { theme: config === null || config === void 0 ? void 0 : config.theme }), require$$0.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 }), require$$0.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 }), require$$0.jsx("div", { className: "xa-spinner-container ".concat(visible && connectionStatus === "pending" ? "visible" : ""), children: require$$0.jsx("div", { className: "xa-spinner" }) }), connectionStatus === "offline" && require$$0.jsx(ServerOffline, {}), chatState.wsButton && visible && (require$$0.jsx(WsButton, { button: chatState.wsButton, onPress: handleWsButtonPress })), require$$0.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 }), require$$0.jsx("div", { className: "restartModal", ref: modalRef, onClick: handleRestartModalCloseClick, children: require$$0.jsx(ModalContent, { onClose: handleRestartModalCloseClick, onReset: handleReset }) })] }), actionBarEnabled && config.actionBar ? (require$$0.jsxs(require$$0.Fragment, { children: [require$$0.jsx(ActionBar, { config: __assign(__assign({}, config.actionBar), {
35866
36194
  // Use actionBar.cta if set, otherwise fall back to widget-level cta
35867
- cta: (_11 = config.actionBar.cta) !== null && _11 !== void 0 ? _11 : config.cta }), visible: visible, chatDisabled: config.disabled, hasUserInteracted: (_12 = chatState.visuals) === null || _12 === void 0 ? void 0 : _12.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (require$$0.jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (require$$0.jsx(ChatButton, { addClass: visible ? "visible" : "", onClick: chatButtonOnClick, config: config === null || config === void 0 ? void 0 : config.cta, 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 })), require$$0.jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
36195
+ cta: (_10 = config.actionBar.cta) !== null && _10 !== void 0 ? _10 : config.cta }), visible: visible, chatDisabled: config.disabled, hasUserInteracted: (_11 = chatState.visuals) === null || _11 === void 0 ? void 0 : _11.hasInteracted, onChatClick: chatButtonOnClick, onChatMinimize: handleMinimizeClick, onFormClick: handleFormButtonClick, onCtaDismiss: handleCtaDismiss }), formModalOpen && activeFormConfig && (require$$0.jsx(FormModal, { config: activeFormConfig, widgetEnv: config, onClose: handleFormModalClose }))] })) : (require$$0.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 })), require$$0.jsx(ErrorOverlay, { enableErrorOverlay: config === null || config === void 0 ? void 0 : config.enableErrorOverlay })] }));
35868
36196
  };
35869
36197
  /**
35870
36198
  * Top-level wrapper that dispatches between preview mode and connected mode.
@@ -35957,9 +36285,10 @@ var LocalStorage = /** @class */ (function () {
35957
36285
  function persistStateReducer(storage, initialState, innerReducer) {
35958
36286
  var _a;
35959
36287
  var obj = (_a = storage.get()) !== null && _a !== void 0 ? _a : initialState;
35960
- // Clean up hasInteracted flag on page load - don't persist across browser refreshes
36288
+ // Clean up transient visual state on page load - don't persist across browser refreshes
36289
+ // visible is derived from config (autoOpenOnWidth, autoOpenOnPattern) and visuals.opened
35961
36290
  if (obj && obj.visuals) {
35962
- obj = __assign(__assign({}, obj), { visuals: __assign(__assign({}, obj.visuals), { hasInteracted: undefined }) });
36291
+ obj = __assign(__assign({}, obj), { visuals: __assign(__assign({}, obj.visuals), { hasInteracted: undefined, visible: undefined }) });
35963
36292
  }
35964
36293
  return function (state, action) {
35965
36294
  if (state === void 0) { state = obj; }
@@ -36076,6 +36405,20 @@ function resetReducer(state) {
36076
36405
  function isChatSystemMessage(chat) {
36077
36406
  return chat.type === "chat.systemmessage";
36078
36407
  }
36408
+ /**
36409
+ * Find the LAST streaming message index. We must search from the end because
36410
+ * orphan isStreaming messages from interrupted streams may exist earlier in the
36411
+ * array. The active streaming message is always the most recently inserted one.
36412
+ */
36413
+ function findLastStreamingIndex(chats) {
36414
+ for (var i = chats.length - 1; i >= 0; i--) {
36415
+ var chat = chats[i];
36416
+ if (isChatMsgDetail(chat) && chat.isStreaming) {
36417
+ return i;
36418
+ }
36419
+ }
36420
+ return -1;
36421
+ }
36079
36422
  // todo: create reducer (requires redux-thunk dependency)
36080
36423
  function appendMessageToState(state, msg) {
36081
36424
  state.chats = joinMessages(state.chats, msg);
@@ -36193,8 +36536,19 @@ function update(state, action) {
36193
36536
  case "chat.typing":
36194
36537
  var agent = state.agents[action.detail.user.nick];
36195
36538
  return __assign(__assign({}, state), { lastTimestamp: (_v = action.detail) === null || _v === void 0 ? void 0 : _v.timestamp, agents: __assign(__assign({}, state.agents), (_d = {}, _d[action.detail.user.nick] = __assign(__assign({}, agent), { typing: action.detail.typing }), _d)) });
36196
- case "chat.stream.start":
36197
- // Add placeholder message with streaming state
36539
+ case "chat.stream.start": {
36540
+ // Clean up any orphan streaming messages from interrupted streams
36541
+ // before creating the new placeholder.
36542
+ var cleanedChats = newState.chats.map(function (chat) {
36543
+ if (isChatMsgDetail(chat) && chat.isStreaming) {
36544
+ return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
36545
+ }
36546
+ return chat;
36547
+ });
36548
+ // Add placeholder message with streaming state.
36549
+ // Use joinMessages (insertSorted by timestamp) instead of append to
36550
+ // guarantee correct ordering even if events arrive out of order
36551
+ // (e.g., after an SSE abort/retry during tab backgrounding).
36198
36552
  var streamingMsg = {
36199
36553
  type: "chat.msg",
36200
36554
  user: action.detail.user,
@@ -36204,13 +36558,15 @@ function update(state, action) {
36204
36558
  },
36205
36559
  isStreaming: true,
36206
36560
  };
36207
- return __assign(__assign({}, newState), { chats: __spreadArray$1(__spreadArray$1([], newState.chats, true), [streamingMsg], false), streamingMessageId: action.detail.messageId });
36561
+ var newChats = joinMessages(cleanedChats, streamingMsg);
36562
+ return __assign(__assign({}, newState), { chats: newChats, streamingMessageId: action.detail.messageId });
36563
+ }
36208
36564
  case "chat.stream.chunk": {
36209
36565
  // Find the streaming message by ID and append text
36210
36566
  var chunkDetail = action.detail;
36211
36567
  if (state.streamingMessageId === chunkDetail.messageId) {
36212
36568
  // Find the streaming message by ID (not by index - other messages could be inserted)
36213
- var streamingIndex_1 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36569
+ var streamingIndex_1 = findLastStreamingIndex(state.chats);
36214
36570
  if (streamingIndex_1 >= 0) {
36215
36571
  var chunkText_1 = chunkDetail.text;
36216
36572
  return __assign(__assign({}, state), { lastTimestamp: chunkDetail === null || chunkDetail === void 0 ? void 0 : chunkDetail.timestamp, chats: state.chats.map(function (chat, index) {
@@ -36230,7 +36586,7 @@ function update(state, action) {
36230
36586
  var toolStartDetail_1 = action.detail;
36231
36587
  if (state.streamingMessageId === toolStartDetail_1.messageId) {
36232
36588
  // Find the streaming message by ID (not by index - other messages could be inserted)
36233
- var streamingIndex_2 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36589
+ var streamingIndex_2 = findLastStreamingIndex(state.chats);
36234
36590
  if (streamingIndex_2 >= 0) {
36235
36591
  return __assign(__assign({}, state), { lastTimestamp: toolStartDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
36236
36592
  if (index === streamingIndex_2 && isChatMsgDetail(chat)) {
@@ -36260,7 +36616,7 @@ function update(state, action) {
36260
36616
  var toolEndDetail_1 = action.detail;
36261
36617
  if (state.streamingMessageId === toolEndDetail_1.messageId) {
36262
36618
  // Find the streaming message by ID (not by index - other messages could be inserted)
36263
- var streamingIndex_3 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36619
+ var streamingIndex_3 = findLastStreamingIndex(state.chats);
36264
36620
  if (streamingIndex_3 >= 0) {
36265
36621
  return __assign(__assign({}, state), { lastTimestamp: toolEndDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
36266
36622
  var _a;
@@ -36289,7 +36645,7 @@ function update(state, action) {
36289
36645
  var endDetail = action.detail;
36290
36646
  if (state.streamingMessageId === endDetail.messageId) {
36291
36647
  // Find the streaming message by ID (not by index - other messages could be inserted)
36292
- var streamingIndex_4 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36648
+ var streamingIndex_4 = findLastStreamingIndex(state.chats);
36293
36649
  var endStreamingChat = streamingIndex_4 >= 0 ? state.chats[streamingIndex_4] : undefined;
36294
36650
  // Verify it's the right message using type guard
36295
36651
  if (streamingIndex_4 >= 0 && endStreamingChat && isChatMsgDetail(endStreamingChat)) {
@@ -36308,6 +36664,51 @@ function update(state, action) {
36308
36664
  }
36309
36665
  return state;
36310
36666
  }
36667
+ case "chat.stream.abort": {
36668
+ // Finalize a streaming message interrupted by connection loss during sleep.
36669
+ // Keeps whatever partial text is already in the message.
36670
+ var abortDetail = action.detail;
36671
+ if (state.streamingMessageId === abortDetail.messageId) {
36672
+ var streamingIndex_5 = findLastStreamingIndex(state.chats);
36673
+ if (streamingIndex_5 >= 0) {
36674
+ return __assign(__assign({}, state), { lastTimestamp: abortDetail.timestamp, streamingMessageId: undefined, chats: state.chats.map(function (chat, index) {
36675
+ if (index === streamingIndex_5 && isChatMsgDetail(chat)) {
36676
+ return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
36677
+ }
36678
+ return chat;
36679
+ }) });
36680
+ }
36681
+ }
36682
+ return state;
36683
+ }
36684
+ case "chat.stream.resume": {
36685
+ // Re-enable streaming on the last aborted bot message after replay reconnect.
36686
+ // Targets the most recent wasStreamed message that is NOT currently streaming
36687
+ // (i.e., the one finalized by chat.stream.abort). Updates streamingMessageId
36688
+ // so replayed chunks route to the existing message.
36689
+ // Guard: only resume if no stream is currently active.
36690
+ var resumeDetail = action.detail;
36691
+ if (state.streamingMessageId) {
36692
+ return state;
36693
+ }
36694
+ var lastBotIndex_1 = -1;
36695
+ for (var i = state.chats.length - 1; i >= 0; i--) {
36696
+ var chat = state.chats[i];
36697
+ if (isChatMsgDetail(chat) && chat.wasStreamed && !chat.isStreaming) {
36698
+ lastBotIndex_1 = i;
36699
+ break;
36700
+ }
36701
+ }
36702
+ if (lastBotIndex_1 >= 0) {
36703
+ return __assign(__assign({}, state), { lastTimestamp: resumeDetail.timestamp, streamingMessageId: resumeDetail.messageId, chats: state.chats.map(function (chat, index) {
36704
+ if (index === lastBotIndex_1 && isChatMsgDetail(chat)) {
36705
+ return __assign(__assign({}, chat), { isStreaming: true, wasStreamed: false });
36706
+ }
36707
+ return chat;
36708
+ }) });
36709
+ }
36710
+ return state;
36711
+ }
36311
36712
  default:
36312
36713
  return state;
36313
36714
  }