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