@xapp/chat-widget 1.88.0 → 1.91.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
@@ -1899,7 +1899,15 @@ function sendGreeting(PRECHAT_FORM_ENABLED) {
1899
1899
  }
1900
1900
 
1901
1901
  function sendMessage(msg) {
1902
- return function (chatServer) { return function () {
1902
+ return function (chatServer) { return function (dispatch, getState) {
1903
+ var state = getState();
1904
+ // Don't allow visitor to send message if offline
1905
+ if (state.accountStatus === "offline" && !state.isChatting)
1906
+ return;
1907
+ // Write user message to Redux before server send so it appears above streaming response
1908
+ if (msg.msg) {
1909
+ dispatch(writeMessage(msg.msg, state.visitor, +new Date()));
1910
+ }
1903
1911
  chatServer.sendChatMsgRequest(msg, function () { });
1904
1912
  }; };
1905
1913
  }
@@ -2984,7 +2992,9 @@ function setSessionId(sessionId) {
2984
2992
 
2985
2993
  // Endpoint path constants
2986
2994
  var HOMEOWNER_ENDPOINT_PATH = "/chat/stream";
2995
+ var HOMEOWNER_CLOSE_ENDPOINT_PATH = "/chat/close";
2987
2996
  var BUSINESS_OWNER_ENDPOINT_PATH = "/api/chat/stream";
2997
+ var BUSINESS_OWNER_CLOSE_ENDPOINT_PATH = "/api/chat/close";
2988
2998
  // Default retry configuration for MCP connections
2989
2999
  var DEFAULT_MCP_RETRY_CONFIG = [
2990
3000
  { retry: 0, delay: 0, text: "" }, // Initial attempt (silent)
@@ -3138,6 +3148,22 @@ function clearStoredLastSeq(sessionId) {
3138
3148
  // sessionStorage may be unavailable
3139
3149
  }
3140
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
+ }
3141
3167
  /**
3142
3168
  * Gets the current page context from browser globals.
3143
3169
  * If an override is provided, uses that instead (useful for testing).
@@ -3146,7 +3172,7 @@ function clearStoredLastSeq(sessionId) {
3146
3172
  * may contain sensitive data (tokens, PII). Only origin + pathname are sent.
3147
3173
  */
3148
3174
  function getPageContext(override) {
3149
- var localTime = new Date().toISOString();
3175
+ var localTime = getLocalISOString();
3150
3176
  // If override provided, merge with defaults (override wins)
3151
3177
  // Overrides are typically for testing and may include full URLs
3152
3178
  if (override && Object.keys(override).length > 0) {
@@ -3198,12 +3224,15 @@ var MCPChat = /** @class */ (function () {
3198
3224
  this.isValidatingSession = false;
3199
3225
  // Tool call tracking for debug mode
3200
3226
  this.currentToolCalls = [];
3201
- // TODO: isReplaying and lastSeq are tracked for future replay support.
3202
- // Wire up replay requests in wakeup() once the server endpoint supports it.
3203
3227
  this.isReplaying = false;
3228
+ // Sleep/wakeup state for tab backgrounding recovery
3229
+ this.isSleeping = false;
3230
+ this.isRequestingReplay = false;
3231
+ this.needsReplayOnWakeup = false;
3204
3232
  // True when userId was assigned by the server (via connected SSE event),
3205
3233
  // meaning it should not be overwritten by the default client fingerprint.
3206
3234
  this.userIdIsServerAssigned = false;
3235
+ this.sentMessageCount = 0;
3207
3236
  this.config = config;
3208
3237
  this.options = options;
3209
3238
  this.configurableMessages = options === null || options === void 0 ? void 0 : options.configurableMessages;
@@ -3239,6 +3268,7 @@ var MCPChat = /** @class */ (function () {
3239
3268
  }
3240
3269
  MCPChat.prototype.init = function (dispatch) {
3241
3270
  this.dispatch = dispatch;
3271
+ this.sessionStartTime = Date.now();
3242
3272
  var mode = this.config.mode || "homeowner";
3243
3273
  log("MCPChat initialized with mode: ".concat(mode));
3244
3274
  // Warn if not using HTTPS with authentication
@@ -3319,7 +3349,9 @@ var MCPChat = /** @class */ (function () {
3319
3349
  // Page context
3320
3350
  pageContext: getPageContext(this.config.pageContextOverride),
3321
3351
  // Client timestamp for latency measurement and clock drift detection
3322
- clientTime: new Date().toISOString() }, additionalData);
3352
+ clientTime: new Date().toISOString(),
3353
+ // Last sequence number for replay on reconnect
3354
+ lastSeq: this.lastSeq }, additionalData);
3323
3355
  };
3324
3356
  /**
3325
3357
  * Initializes a new session by calling the endpoint with { newSession: true }.
@@ -3392,6 +3424,14 @@ var MCPChat = /** @class */ (function () {
3392
3424
  // homeowner mode - serverUrl should already include appId
3393
3425
  return "".concat(serverUrl).concat(HOMEOWNER_ENDPOINT_PATH);
3394
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
+ };
3395
3435
  MCPChat.prototype.sendNewMessage = function (msg, user) {
3396
3436
  log("SERVER: new message: ".concat(JSON.stringify(msg)));
3397
3437
  this.dispatch({
@@ -3542,6 +3582,30 @@ var MCPChat = /** @class */ (function () {
3542
3582
  },
3543
3583
  });
3544
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
+ };
3545
3609
  MCPChat.prototype.dispatchToolStart = function (params) {
3546
3610
  this.dispatch({
3547
3611
  type: "chat",
@@ -3855,7 +3919,10 @@ var MCPChat = /** @class */ (function () {
3855
3919
  attempt++;
3856
3920
  return [3 /*break*/, 3];
3857
3921
  case 14:
3858
- if (!success) {
3922
+ if (success) {
3923
+ this.sentMessageCount++;
3924
+ }
3925
+ else {
3859
3926
  log("All MCP retry attempts exhausted");
3860
3927
  }
3861
3928
  return [2 /*return*/];
@@ -3907,6 +3974,10 @@ var MCPChat = /** @class */ (function () {
3907
3974
  : {})),
3908
3975
  body: JSON.stringify(body),
3909
3976
  signal: _this.abortController.signal,
3977
+ // Keep the connection open when the tab is hidden. Without this,
3978
+ // fetchEventSource closes and re-POSTs on visibility change, which
3979
+ // creates duplicate streaming messages and out-of-order responses.
3980
+ openWhenHidden: true,
3910
3981
  onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
3911
3982
  var error;
3912
3983
  return __generator$1(this, function (_a) {
@@ -3923,6 +3994,11 @@ var MCPChat = /** @class */ (function () {
3923
3994
  });
3924
3995
  }); },
3925
3996
  onmessage: function (event) {
3997
+ // Guard: ignore events from stale/aborted requests
3998
+ if (_this.currentMessageId !== messageId) {
3999
+ log("[SSE] Ignoring stale event for messageId: ".concat(messageId, ", current: ").concat(_this.currentMessageId));
4000
+ return;
4001
+ }
3926
4002
  log("SSE event: ".concat(event.event, ", data: ").concat(event.data));
3927
4003
  var eventType = event.event || "message";
3928
4004
  var data = {};
@@ -3962,11 +4038,17 @@ var MCPChat = /** @class */ (function () {
3962
4038
  storeIdentity(data.userId, data.sessionId);
3963
4039
  log("[SSE] Identity from connected - userId: ".concat(data.userId, ", sessionId: ").concat(data.sessionId));
3964
4040
  }
3965
- // Always start streaming - server sends greeting for both new and returning users
3966
- // (resumeSession requests always get a personalized greeting)
3967
4041
  _this.currentToolCalls = [];
3968
4042
  _this.stopTyping();
3969
- _this.dispatchStreamStart(messageId);
4043
+ if (data.isReplay) {
4044
+ // Replay connection — resume into the existing partial
4045
+ // message instead of creating a duplicate placeholder
4046
+ _this.dispatchStreamResume(messageId);
4047
+ }
4048
+ else {
4049
+ // Normal connection — create new streaming placeholder
4050
+ _this.dispatchStreamStart(messageId);
4051
+ }
3970
4052
  _this.startStreamThrottle(messageId);
3971
4053
  break;
3972
4054
  case "session_expired":
@@ -3989,9 +4071,9 @@ var MCPChat = /** @class */ (function () {
3989
4071
  if (data.text) {
3990
4072
  log("[SSE] Text chunk received (".concat(data.text.length, " chars), total chunks: ").concat(textChunks.length + 1, ", isReplay: ").concat(data.isReplay));
3991
4073
  textChunks.push(data.text);
3992
- if (data.isReplay) {
3993
- // Replayed text — dispatch immediately (no throttle)
3994
- // so the user sees caught-up content instantly
4074
+ if (data.isReplay || _this.isSleeping) {
4075
+ // Replayed text or tab hidden — dispatch immediately
4076
+ // (no throttle timer running in either case)
3995
4077
  _this.dispatchStreamChunk(messageId, data.text);
3996
4078
  }
3997
4079
  else {
@@ -4088,6 +4170,7 @@ var MCPChat = /** @class */ (function () {
4088
4170
  text: accumulatedText || data.response.response || "",
4089
4171
  options: options,
4090
4172
  sources: sources,
4173
+ displays: data.response.displays,
4091
4174
  };
4092
4175
  // Capture tool calls before flushing (debug mode)
4093
4176
  var toolCalls_1 = _this.config.debugMode ? __spreadArray$1([], _this.currentToolCalls, true) : undefined;
@@ -4125,7 +4208,20 @@ var MCPChat = /** @class */ (function () {
4125
4208
  if (_this.currentMessageId === messageId && !hasError) {
4126
4209
  hasError = true;
4127
4210
  _this.isReplaying = false;
4128
- _this.stopStreamThrottle();
4211
+ if (_this.isSleeping) {
4212
+ // Connection dropped while tab is hidden — don't treat as
4213
+ // normal error. Finalize partial message and request replay
4214
+ // when the user returns.
4215
+ log("[MCPChat] SSE error during sleep - will request replay on wakeup");
4216
+ _this.needsReplayOnWakeup = true;
4217
+ if (_this.streamingMessageIdForThrottle) {
4218
+ _this.dispatchStreamAbort(messageId);
4219
+ }
4220
+ _this.stopStreamThrottle();
4221
+ }
4222
+ else {
4223
+ _this.stopStreamThrottle();
4224
+ }
4129
4225
  sseError = error instanceof Error ? error : new Error(String(error));
4130
4226
  // Get status code if available
4131
4227
  var statusCode = (error === null || error === void 0 ? void 0 : error.statusCode) || httpStatusCode;
@@ -4194,23 +4290,138 @@ var MCPChat = /** @class */ (function () {
4194
4290
  }
4195
4291
  };
4196
4292
  MCPChat.prototype.sleep = function () {
4197
- this.stopStreamThrottle();
4198
- this.isReplaying = false;
4199
- if (this.abortController) {
4200
- this.abortController.abort();
4201
- this.abortController = undefined;
4293
+ this.isSleeping = true;
4294
+ if (this.streamingMessageIdForThrottle) {
4295
+ // Active stream in progress — keep SSE connection alive so the
4296
+ // response completes naturally while the tab is hidden.
4297
+ this.needsReplayOnWakeup = false;
4298
+ log("[MCPChat] sleep() - stream in progress, keeping SSE connection alive");
4299
+ // Flush any buffered text to Redux immediately
4300
+ if (this.streamBuffer.length > 0) {
4301
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4302
+ this.streamBuffer = "";
4303
+ }
4304
+ // Stop the visual throttle timer (browsers throttle timers anyway)
4305
+ // but keep streamingMessageIdForThrottle so incoming chunks can be dispatched
4306
+ if (this.streamThrottleTimer) {
4307
+ clearInterval(this.streamThrottleTimer);
4308
+ this.streamThrottleTimer = undefined;
4309
+ }
4310
+ if (this.visibilityHandler) {
4311
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
4312
+ this.visibilityHandler = undefined;
4313
+ }
4314
+ }
4315
+ else {
4316
+ // No active stream — safe to abort the idle connection
4317
+ this.stopStreamThrottle();
4318
+ this.isReplaying = false;
4319
+ if (this.abortController) {
4320
+ this.abortController.abort();
4321
+ this.abortController = undefined;
4322
+ }
4202
4323
  }
4203
4324
  };
4204
4325
  MCPChat.prototype.wakeup = function () {
4205
- // When tab is foregrounded, just restore connection status.
4206
- // Don't validate/replay yet — the session will be validated
4207
- // on the next user message. This avoids creating duplicate
4208
- // greetings if validation fails transiently.
4326
+ this.isSleeping = false;
4327
+ if (this.streamingMessageIdForThrottle && this.abortController) {
4328
+ // Stream is still in progress — restart the visual throttle
4329
+ log("[MCPChat] wakeup() - stream still in progress, restarting throttle");
4330
+ // Flush accumulated buffer before startStreamThrottle (which resets it)
4331
+ if (this.streamBuffer.length > 0) {
4332
+ this.dispatchStreamChunk(this.streamingMessageIdForThrottle, this.streamBuffer);
4333
+ this.streamBuffer = "";
4334
+ }
4335
+ this.startStreamThrottle(this.streamingMessageIdForThrottle);
4336
+ }
4337
+ else if (this.needsReplayOnWakeup) {
4338
+ this.needsReplayOnWakeup = false;
4339
+ if (this.lastSeq !== undefined && this._sessionId) {
4340
+ // Connection dropped during sleep — request replay of missed events.
4341
+ // Don't set "online" yet — requestReplay is fire-and-forget and the
4342
+ // connection status will be updated by the SSE onopen handler.
4343
+ log("[MCPChat] wakeup() - connection lost during sleep, requesting replay");
4344
+ this.requestReplay();
4345
+ return;
4346
+ }
4347
+ // Cannot replay — missing lastSeq or sessionId
4348
+ err("[MCPChat] wakeup() - cannot replay, missing lastSeq (".concat(this.lastSeq, ") or sessionId (").concat(this._sessionId, ")"));
4349
+ }
4209
4350
  if (this._sessionId) {
4210
4351
  log("[MCPChat] Wakeup - session exists, lastSeq: ".concat(this.lastSeq));
4211
4352
  this.setConnectionStatus("online");
4212
4353
  }
4213
4354
  };
4355
+ MCPChat.prototype.requestReplay = function () {
4356
+ return __awaiter$1(this, void 0, void 0, function () {
4357
+ var error_5;
4358
+ return __generator$1(this, function (_a) {
4359
+ switch (_a.label) {
4360
+ case 0:
4361
+ if (this.isRequestingReplay) {
4362
+ log("[MCPChat] Replay already in progress, skipping");
4363
+ return [2 /*return*/];
4364
+ }
4365
+ this.isRequestingReplay = true;
4366
+ this.typing();
4367
+ _a.label = 1;
4368
+ case 1:
4369
+ _a.trys.push([1, 3, 4, 5]);
4370
+ // lastSeq is already included by buildRequestPayload
4371
+ return [4 /*yield*/, this.performSSERequest(this.buildRequestPayload())];
4372
+ case 2:
4373
+ // lastSeq is already included by buildRequestPayload
4374
+ _a.sent();
4375
+ log("[MCPChat] Replay request completed");
4376
+ return [3 /*break*/, 5];
4377
+ case 3:
4378
+ error_5 = _a.sent();
4379
+ err("[MCPChat] Replay request failed: ".concat(error_5));
4380
+ this.setConnectionStatus("offline");
4381
+ return [3 /*break*/, 5];
4382
+ case 4:
4383
+ this.stopTyping();
4384
+ this.isRequestingReplay = false;
4385
+ return [7 /*endfinally*/];
4386
+ case 5: return [2 /*return*/];
4387
+ }
4388
+ });
4389
+ });
4390
+ };
4391
+ /**
4392
+ * Sends a best-effort close notification to the agent server.
4393
+ * Uses navigator.sendBeacon() so the request survives page unload.
4394
+ * Called from the beforeunload event handler and the widget close button.
4395
+ */
4396
+ MCPChat.prototype.notifyDisconnecting = function (reason) {
4397
+ if (reason === void 0) { reason = "close_tab"; }
4398
+ if (!this._sessionId) {
4399
+ return;
4400
+ }
4401
+ var endpoint = this.getCloseEndpoint();
4402
+ var payload = JSON.stringify({
4403
+ sessionId: this._sessionId,
4404
+ userId: this._userId || undefined,
4405
+ reason: reason,
4406
+ messageCount: this.sentMessageCount,
4407
+ durationMs: this.sessionStartTime !== undefined ? Date.now() - this.sessionStartTime : 0,
4408
+ });
4409
+ log("[MCPChat] notifyDisconnecting: reason=".concat(reason, ", endpoint=").concat(endpoint));
4410
+ // For widget_close, the page is not unloading so fetch() can be used — it supports
4411
+ // auth headers (needed for business_owner mode) unlike sendBeacon.
4412
+ if (reason === "widget_close" && typeof fetch !== "undefined") {
4413
+ var headers = { "Content-Type": "application/json" };
4414
+ if (this.config.authToken) {
4415
+ headers["Authorization"] = "Bearer ".concat(this.config.authToken);
4416
+ }
4417
+ fetch(endpoint, { method: "POST", headers: headers, body: payload, keepalive: true });
4418
+ return;
4419
+ }
4420
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
4421
+ var blob = new Blob([payload], { type: "application/json" });
4422
+ navigator.sendBeacon(endpoint, blob);
4423
+ }
4424
+ };
4214
4425
  MCPChat.prototype.startSession = function (sessionId) {
4215
4426
  // userId priority:
4216
4427
  // 1. Existing server-assigned userId (from localStorage or previous connected event)
@@ -35558,7 +35769,7 @@ var ChatWidgetUI = function (props) {
35558
35769
  var handleBeforeUnload = function () {
35559
35770
  log("beforeunload - notifying router of user disconnecting");
35560
35771
  if (chatServer.notifyDisconnecting) {
35561
- chatServer.notifyDisconnecting();
35772
+ chatServer.notifyDisconnecting("close_tab");
35562
35773
  }
35563
35774
  };
35564
35775
  window.addEventListener("beforeunload", handleBeforeUnload);
@@ -35712,6 +35923,9 @@ var ChatWidgetUI = function (props) {
35712
35923
  }
35713
35924
  /** Called when cancel is clicked */
35714
35925
  function handleCancelClick() {
35926
+ if (chatServer === null || chatServer === void 0 ? void 0 : chatServer.notifyDisconnecting) {
35927
+ chatServer.notifyDisconnecting("widget_close");
35928
+ }
35715
35929
  // First reset to clear all state
35716
35930
  innerDispatch(reset());
35717
35931
  // Then set hasInteracted to prevent CTA from showing after cancel
@@ -36066,6 +36280,20 @@ function resetReducer(state) {
36066
36280
  function isChatSystemMessage(chat) {
36067
36281
  return chat.type === "chat.systemmessage";
36068
36282
  }
36283
+ /**
36284
+ * Find the LAST streaming message index. We must search from the end because
36285
+ * orphan isStreaming messages from interrupted streams may exist earlier in the
36286
+ * array. The active streaming message is always the most recently inserted one.
36287
+ */
36288
+ function findLastStreamingIndex(chats) {
36289
+ for (var i = chats.length - 1; i >= 0; i--) {
36290
+ var chat = chats[i];
36291
+ if (isChatMsgDetail(chat) && chat.isStreaming) {
36292
+ return i;
36293
+ }
36294
+ }
36295
+ return -1;
36296
+ }
36069
36297
  // todo: create reducer (requires redux-thunk dependency)
36070
36298
  function appendMessageToState(state, msg) {
36071
36299
  state.chats = joinMessages(state.chats, msg);
@@ -36183,8 +36411,19 @@ function update(state, action) {
36183
36411
  case "chat.typing":
36184
36412
  var agent = state.agents[action.detail.user.nick];
36185
36413
  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)) });
36186
- case "chat.stream.start":
36187
- // Add placeholder message with streaming state
36414
+ case "chat.stream.start": {
36415
+ // Clean up any orphan streaming messages from interrupted streams
36416
+ // before creating the new placeholder.
36417
+ var cleanedChats = newState.chats.map(function (chat) {
36418
+ if (isChatMsgDetail(chat) && chat.isStreaming) {
36419
+ return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
36420
+ }
36421
+ return chat;
36422
+ });
36423
+ // Add placeholder message with streaming state.
36424
+ // Use joinMessages (insertSorted by timestamp) instead of append to
36425
+ // guarantee correct ordering even if events arrive out of order
36426
+ // (e.g., after an SSE abort/retry during tab backgrounding).
36188
36427
  var streamingMsg = {
36189
36428
  type: "chat.msg",
36190
36429
  user: action.detail.user,
@@ -36194,13 +36433,15 @@ function update(state, action) {
36194
36433
  },
36195
36434
  isStreaming: true,
36196
36435
  };
36197
- return __assign(__assign({}, newState), { chats: __spreadArray$1(__spreadArray$1([], newState.chats, true), [streamingMsg], false), streamingMessageId: action.detail.messageId });
36436
+ var newChats = joinMessages(cleanedChats, streamingMsg);
36437
+ return __assign(__assign({}, newState), { chats: newChats, streamingMessageId: action.detail.messageId });
36438
+ }
36198
36439
  case "chat.stream.chunk": {
36199
36440
  // Find the streaming message by ID and append text
36200
36441
  var chunkDetail = action.detail;
36201
36442
  if (state.streamingMessageId === chunkDetail.messageId) {
36202
36443
  // Find the streaming message by ID (not by index - other messages could be inserted)
36203
- var streamingIndex_1 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36444
+ var streamingIndex_1 = findLastStreamingIndex(state.chats);
36204
36445
  if (streamingIndex_1 >= 0) {
36205
36446
  var chunkText_1 = chunkDetail.text;
36206
36447
  return __assign(__assign({}, state), { lastTimestamp: chunkDetail === null || chunkDetail === void 0 ? void 0 : chunkDetail.timestamp, chats: state.chats.map(function (chat, index) {
@@ -36220,7 +36461,7 @@ function update(state, action) {
36220
36461
  var toolStartDetail_1 = action.detail;
36221
36462
  if (state.streamingMessageId === toolStartDetail_1.messageId) {
36222
36463
  // Find the streaming message by ID (not by index - other messages could be inserted)
36223
- var streamingIndex_2 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36464
+ var streamingIndex_2 = findLastStreamingIndex(state.chats);
36224
36465
  if (streamingIndex_2 >= 0) {
36225
36466
  return __assign(__assign({}, state), { lastTimestamp: toolStartDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
36226
36467
  if (index === streamingIndex_2 && isChatMsgDetail(chat)) {
@@ -36250,7 +36491,7 @@ function update(state, action) {
36250
36491
  var toolEndDetail_1 = action.detail;
36251
36492
  if (state.streamingMessageId === toolEndDetail_1.messageId) {
36252
36493
  // Find the streaming message by ID (not by index - other messages could be inserted)
36253
- var streamingIndex_3 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36494
+ var streamingIndex_3 = findLastStreamingIndex(state.chats);
36254
36495
  if (streamingIndex_3 >= 0) {
36255
36496
  return __assign(__assign({}, state), { lastTimestamp: toolEndDetail_1.timestamp, chats: state.chats.map(function (chat, index) {
36256
36497
  var _a;
@@ -36279,7 +36520,7 @@ function update(state, action) {
36279
36520
  var endDetail = action.detail;
36280
36521
  if (state.streamingMessageId === endDetail.messageId) {
36281
36522
  // Find the streaming message by ID (not by index - other messages could be inserted)
36282
- var streamingIndex_4 = state.chats.findIndex(function (chat) { return isChatMsgDetail(chat) && chat.isStreaming; });
36523
+ var streamingIndex_4 = findLastStreamingIndex(state.chats);
36283
36524
  var endStreamingChat = streamingIndex_4 >= 0 ? state.chats[streamingIndex_4] : undefined;
36284
36525
  // Verify it's the right message using type guard
36285
36526
  if (streamingIndex_4 >= 0 && endStreamingChat && isChatMsgDetail(endStreamingChat)) {
@@ -36298,6 +36539,51 @@ function update(state, action) {
36298
36539
  }
36299
36540
  return state;
36300
36541
  }
36542
+ case "chat.stream.abort": {
36543
+ // Finalize a streaming message interrupted by connection loss during sleep.
36544
+ // Keeps whatever partial text is already in the message.
36545
+ var abortDetail = action.detail;
36546
+ if (state.streamingMessageId === abortDetail.messageId) {
36547
+ var streamingIndex_5 = findLastStreamingIndex(state.chats);
36548
+ if (streamingIndex_5 >= 0) {
36549
+ return __assign(__assign({}, state), { lastTimestamp: abortDetail.timestamp, streamingMessageId: undefined, chats: state.chats.map(function (chat, index) {
36550
+ if (index === streamingIndex_5 && isChatMsgDetail(chat)) {
36551
+ return __assign(__assign({}, chat), { isStreaming: false, wasStreamed: true });
36552
+ }
36553
+ return chat;
36554
+ }) });
36555
+ }
36556
+ }
36557
+ return state;
36558
+ }
36559
+ case "chat.stream.resume": {
36560
+ // Re-enable streaming on the last aborted bot message after replay reconnect.
36561
+ // Targets the most recent wasStreamed message that is NOT currently streaming
36562
+ // (i.e., the one finalized by chat.stream.abort). Updates streamingMessageId
36563
+ // so replayed chunks route to the existing message.
36564
+ // Guard: only resume if no stream is currently active.
36565
+ var resumeDetail = action.detail;
36566
+ if (state.streamingMessageId) {
36567
+ return state;
36568
+ }
36569
+ var lastBotIndex_1 = -1;
36570
+ for (var i = state.chats.length - 1; i >= 0; i--) {
36571
+ var chat = state.chats[i];
36572
+ if (isChatMsgDetail(chat) && chat.wasStreamed && !chat.isStreaming) {
36573
+ lastBotIndex_1 = i;
36574
+ break;
36575
+ }
36576
+ }
36577
+ if (lastBotIndex_1 >= 0) {
36578
+ return __assign(__assign({}, state), { lastTimestamp: resumeDetail.timestamp, streamingMessageId: resumeDetail.messageId, chats: state.chats.map(function (chat, index) {
36579
+ if (index === lastBotIndex_1 && isChatMsgDetail(chat)) {
36580
+ return __assign(__assign({}, chat), { isStreaming: true, wasStreamed: false });
36581
+ }
36582
+ return chat;
36583
+ }) });
36584
+ }
36585
+ return state;
36586
+ }
36301
36587
  default:
36302
36588
  return state;
36303
36589
  }