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