bunnyquery 1.11.1 → 1.13.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/engine.cjs CHANGED
@@ -359,10 +359,15 @@ function buildChatSystemPrompt(params) {
359
359
  const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
360
360
  const g = params.indexAccessGroup;
361
361
  const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
362
+ const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
363
+ WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
364
+ KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
365
+ These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
362
366
  let systemPrompt = `
363
367
  You are a dedicated assistant for the project ID: "${projectId}".
364
368
  Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
365
369
  Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
370
+ ${groundingRules}
366
371
  NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
367
372
  CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
368
373
  A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
@@ -627,6 +632,11 @@ function getErrorMessage(input) {
627
632
  }
628
633
  return "Something went wrong.";
629
634
  }
635
+ function isRetriedFailure(input) {
636
+ var envErr = csrEnvelopeError(input);
637
+ if (envErr !== void 0) input = envErr;
638
+ return !!input && typeof input === "object" && input.retried === true;
639
+ }
630
640
  function isErrorResponseBody(response) {
631
641
  var envErr = csrEnvelopeError(response);
632
642
  if (envErr !== void 0) response = envErr;
@@ -3250,8 +3260,9 @@ function mapHistoryListToMessages(list, platform, opts) {
3250
3260
  var createdTs = Number(item && item.created);
3251
3261
  var updatedTs = Number(item && item.updated);
3252
3262
  var executedTs = Number(item && item.executed);
3263
+ var respondedTs = Number(item && item.responded);
3253
3264
  var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
3254
- var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3265
+ var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
3255
3266
  if (userText) {
3256
3267
  var displayContent;
3257
3268
  var indexFile = void 0;
@@ -3298,6 +3309,7 @@ function mapHistoryListToMessages(list, platform, opts) {
3298
3309
  mapped.push(ph);
3299
3310
  } else if (isQueued) ; else if (isErrorResponse) {
3300
3311
  var em = { role: "assistant", content: getErrorMessage(response), isError: true };
3312
+ if (isRetriedFailure(response)) em.isRetried = true;
3301
3313
  if (item._fromBgChain) em._fromBgChain = true;
3302
3314
  if (item._isBgTask) em.isBackgroundTask = true;
3303
3315
  if (serverItemId !== void 0) em._serverItemId = serverItemId;
@@ -3877,6 +3889,13 @@ var ChatSession = class {
3877
3889
  // the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
3878
3890
  /** Live streams by server item id. One per in-flight streamed turn. */
3879
3891
  this.liveStreams = {};
3892
+ /** When skapi recorded a turn's response as having finished arriving, by server
3893
+ * item id. Read off the terminal envelope of a STREAMED turn (the only settle
3894
+ * that carries it, see _settleLiveStream) and used as the answer bubble's time
3895
+ * in place of this browser's clock. Entries are never removed: a settle reaches
3896
+ * here twice (the poll's onResponse, then the promise), and the value for an id
3897
+ * cannot change. */
3898
+ this._respondedAt = {};
3880
3899
  // ─── compact-stub hydration ─────────────────────────────────────────────
3881
3900
  // Split-fetch bg pages arrive as label stubs (no bodies). When the user
3882
3901
  // expands a row, the real reply text is fetched per item (csr-poll point
@@ -4326,7 +4345,9 @@ var ChatSession = class {
4326
4345
  timers = [];
4327
4346
  };
4328
4347
  var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
4348
+ var resolveRaced = null;
4329
4349
  var raced = new Promise(function(resolve, reject) {
4350
+ resolveRaced = resolve;
4330
4351
  base.then(function(res) {
4331
4352
  if (settled) return;
4332
4353
  settled = true;
@@ -4367,9 +4388,16 @@ var ChatSession = class {
4367
4388
  });
4368
4389
  });
4369
4390
  raced.stop = function() {
4391
+ if (settled) return;
4370
4392
  settled = true;
4371
4393
  clearProbes();
4372
- if (stopBase) stopBase();
4394
+ if (stopBase) {
4395
+ try {
4396
+ stopBase();
4397
+ } catch (e) {
4398
+ }
4399
+ }
4400
+ if (resolveRaced) resolveRaced({ id: itemId, status: "stopped" });
4373
4401
  };
4374
4402
  return raced;
4375
4403
  }
@@ -4393,6 +4421,23 @@ var ChatSession = class {
4393
4421
  });
4394
4422
  return n;
4395
4423
  }
4424
+ _noteRespondedAt(itemId, response) {
4425
+ if (!itemId || !isCsrStatusEnvelope(response)) return;
4426
+ var at = Number(response.responded);
4427
+ if (!isFinite(at) || at <= 0) return;
4428
+ if (!this._respondedAt) this._respondedAt = {};
4429
+ this._respondedAt[itemId] = at;
4430
+ }
4431
+ /** The time a live answer bubble is stamped with: when skapi received the
4432
+ * response, if the settle told us, else now. "Now" is what every bubble got
4433
+ * before, and it is still right for a buffered turn, whose settle hands back the
4434
+ * destination's body and nothing of skapi's; a history load then replaces it
4435
+ * with the server's value. */
4436
+ _replyStampFor(msg) {
4437
+ var id = msg ? msg._serverItemId : void 0;
4438
+ var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
4439
+ return typeof at === "number" ? at : wallClockNow();
4440
+ }
4396
4441
  /**
4397
4442
  * Open (or re-open) the live stream for `itemId`, or null when this poll must
4398
4443
  * not carry one.
@@ -4582,6 +4627,7 @@ var ChatSession = class {
4582
4627
  _settleLiveStream(st, response) {
4583
4628
  this._closeLiveStream(st, true);
4584
4629
  if (!isCsrStatusEnvelope(response)) return response;
4630
+ this._noteRespondedAt(st.id, response);
4585
4631
  if (response.status !== "resolved") return response;
4586
4632
  if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
4587
4633
  if (st.finalBody == null) return response;
@@ -5382,6 +5428,7 @@ var ChatSession = class {
5382
5428
  }
5383
5429
  return response;
5384
5430
  }).then(function(response) {
5431
+ if (isPollStopped(response) || self._isCancelledPollResult(response)) return { content: "", isError: false, cancelled: true };
5385
5432
  if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
5386
5433
  var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
5387
5434
  answer = (answer || "").trim();
@@ -5391,6 +5438,10 @@ var ChatSession = class {
5391
5438
  }).then(function(result) {
5392
5439
  delete self.pendingAgentRequests[params.key];
5393
5440
  if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
5441
+ if (result.cancelled) {
5442
+ if (dispatchItemId) self._settleCancelledTurn(dispatchItemId, params.key);
5443
+ return result;
5444
+ }
5394
5445
  var reply = { role: "assistant", content: result.content, isError: result.isError };
5395
5446
  if (dispatchItemId) reply._serverItemId = dispatchItemId;
5396
5447
  self._applyReplyToCache(params.key, reply, dispatchItemId);
@@ -5869,8 +5920,9 @@ var ChatSession = class {
5869
5920
  }
5870
5921
  });
5871
5922
  Promise.resolve(run).catch(function() {
5872
- }).then(function() {
5923
+ }).then(function(result) {
5873
5924
  self.state.sending = false;
5925
+ if (result && result.cancelled) return;
5874
5926
  if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
5875
5927
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
5876
5928
  self.host.scrollToBottomIfSticky(true);
@@ -6017,7 +6069,7 @@ var ChatSession = class {
6017
6069
  return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
6018
6070
  }
6019
6071
  insertAtTarget(msg, targetIdx) {
6020
- if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
6072
+ if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
6021
6073
  var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
6022
6074
  var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
6023
6075
  if (replaceable) this.state.messages[targetIdx] = msg;
@@ -6049,6 +6101,10 @@ var ChatSession = class {
6049
6101
  }
6050
6102
  onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
6051
6103
  if (serverId) this.historyItemPolls.delete(serverId);
6104
+ if (this._isCancelledPollResult(response)) {
6105
+ if (serverId) this._settleCancelledTurn(serverId, ownerKey);
6106
+ return;
6107
+ }
6052
6108
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
6053
6109
  var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
6054
6110
  this._applyReplyToCache(ownerKey, offReply, serverId);
@@ -6248,6 +6304,191 @@ var ChatSession = class {
6248
6304
  }
6249
6305
  });
6250
6306
  }
6307
+ /**
6308
+ * Stop waiting for the reply to the chat turn that is running now, and move on.
6309
+ *
6310
+ * `msg` is the turn's pending ASSISTANT bubble (the dot-trail "Thinking"
6311
+ * placeholder), which is where both views draw the control. A turn's two bubbles
6312
+ * carry the same _serverItemId, so it is also how the request is found.
6313
+ * cancelQueuedMessage is the sibling for a turn still waiting in the queue and
6314
+ * takes the USER bubble; the two differ in what they are handed and in what has
6315
+ * to be unwound afterwards, not in what the server is asked.
6316
+ *
6317
+ * What ends the wait:
6318
+ * 1. csr-cancel marks the running row cancelled. The worker stops relaying and
6319
+ * never stores an answer for it, so the reply cannot turn up later on this
6320
+ * device or another, and history replays the turn as cancelled;
6321
+ * 2. the poll on it is stopped, which settles whichever chain was awaiting it
6322
+ * with a stopped result: dispatchAgentRequest's for an immediate send, the
6323
+ * queued chain for a promoted turn, the history poll after a reload. Each
6324
+ * reads a stopped or cancelled result as "no reply" and releases what it
6325
+ * holds (state.sending, pendingAgentRequests, historyItemPolls), which is
6326
+ * what lets the next send go out immediately instead of queueing;
6327
+ * 3. the bubbles settle at once, without waiting for that chain: the user
6328
+ * bubble takes its cancelled form, the placeholder goes, and the next queued
6329
+ * turn, if there is one, is promoted so its own "Thinking" appears.
6330
+ *
6331
+ * What it does NOT do: the provider call already in flight on the worker runs
6332
+ * to its end (a streamed one stops within about a second). A message sent
6333
+ * behind it on the same queue starts once the worker acks the cancelled row.
6334
+ *
6335
+ * Refused without a server id (the ack has not come back yet; the views disable
6336
+ * the control until it has) and while a cancel is already out. Indexing passes
6337
+ * are not this method's business: their Stop is cancelIndexingGroup, which ends
6338
+ * the file's whole chain.
6339
+ */
6340
+ cancelPendingReply(msg, idx) {
6341
+ var self = this;
6342
+ var id = this.host.getIdentity();
6343
+ var serverId = msg._serverItemId;
6344
+ if (!serverId || msg._cancelling) return;
6345
+ if (msg.role !== "assistant" || !msg.isPending || msg.isBackgroundTask) return;
6346
+ var platform = id.platform;
6347
+ if (platform !== "claude" && platform !== "openai") return;
6348
+ var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
6349
+ var queueBase = id.userId || id.projectId;
6350
+ var queue = msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
6351
+ var findPlaceholder = function() {
6352
+ return self.state.messages.findIndex(function(m) {
6353
+ return m._serverItemId === serverId && m.role === "assistant" && !!m.isPending;
6354
+ });
6355
+ };
6356
+ var at = this.state.messages[idx] && this.state.messages[idx]._serverItemId === serverId && this.state.messages[idx].role === "assistant" ? idx : findPlaceholder();
6357
+ if (at !== -1) {
6358
+ this.state.messages[at] = Object.assign({}, this.state.messages[at], { _cancelling: true, _cancelError: void 0 });
6359
+ }
6360
+ this.host.notify();
6361
+ var unmark = function(errMsg) {
6362
+ var ci = findPlaceholder();
6363
+ if (ci === -1) return;
6364
+ self.state.messages[ci] = Object.assign({}, self.state.messages[ci], { _cancelling: false, _cancelError: errMsg });
6365
+ self.host.notify();
6366
+ };
6367
+ Promise.resolve(this.host.cancelRequest({
6368
+ url,
6369
+ method: "POST",
6370
+ id: serverId,
6371
+ queue,
6372
+ service: id.projectId,
6373
+ owner: id.owner
6374
+ })).then(function(result) {
6375
+ if (result && result.removed) {
6376
+ self._stopPoll(serverId);
6377
+ self._settleCancelledTurn(serverId);
6378
+ return;
6379
+ }
6380
+ unmark(void 0);
6381
+ }).catch(function(err) {
6382
+ unmark(err && typeof err.message === "string" && err.message ? err.message : "Could not stop this request.");
6383
+ });
6384
+ }
6385
+ /** The cancelled form of a request bubble, with everything that keeps it in
6386
+ * place carried over: the file markers that hold an indexing pass in its
6387
+ * collapsed row, the chat it belongs to, its send time and its render key. */
6388
+ _cancelledCopyOf(m, itemId) {
6389
+ var cancelled = { role: "user", content: m.content, isCancelled: true };
6390
+ if (itemId !== void 0) cancelled._serverItemId = itemId;
6391
+ else if (m._serverItemId !== void 0) cancelled._serverItemId = m._serverItemId;
6392
+ if (m.isBackgroundTask) cancelled.isBackgroundTask = true;
6393
+ if (m._indexFile) cancelled._indexFile = m._indexFile;
6394
+ if (m._useBgQueue) cancelled._useBgQueue = true;
6395
+ if (m._ownerKey !== void 0) cancelled._ownerKey = m._ownerKey;
6396
+ if (m._ts !== void 0) cancelled._ts = m._ts;
6397
+ if (m._localId !== void 0) cancelled._localId = m._localId;
6398
+ return cancelled;
6399
+ }
6400
+ /**
6401
+ * Settle a turn as cancelled wherever it lives, and let the queue move on.
6402
+ *
6403
+ * On the chat on screen: every pending placeholder carrying the id goes (a
6404
+ * history refetch can have re-mapped the running turn into a second one), the
6405
+ * request bubble takes its cancelled form, and the next queued turn is promoted
6406
+ * so the conversation does not sit on an "(In queue)" bubble with nothing
6407
+ * running. For a chat that is NOT on screen (the user moved on while the turn
6408
+ * ran, and it was cancelled from elsewhere) the same edit is made in that chat's
6409
+ * history cache, which is what the next visit renders.
6410
+ *
6411
+ * Idempotent on purpose: a cancel this client made settles the bubbles at once,
6412
+ * and the chain that was awaiting the poll settles again a moment later when the
6413
+ * stop resolves it. The second pass finds nothing to do. Deliberately NOT
6414
+ * _removeStrayPendingAssistants: by then the placeholder on screen is the freshly
6415
+ * promoted turn's, whose user bubble is pending, and the sweep would take it.
6416
+ */
6417
+ _settleCancelledTurn(itemId, ownerKey) {
6418
+ if (!itemId) return;
6419
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
6420
+ this._applyCancelToCache(ownerKey, itemId);
6421
+ return;
6422
+ }
6423
+ var changed = false, wasBg = false;
6424
+ for (var i = this.state.messages.length - 1; i >= 0; i--) {
6425
+ var m = this.state.messages[i];
6426
+ if (!m || m._serverItemId !== itemId) continue;
6427
+ if (m.role === "assistant" && m.isPending) {
6428
+ this.state.messages.splice(i, 1);
6429
+ changed = true;
6430
+ continue;
6431
+ }
6432
+ if (m.role === "user" && !m.isCancelled) {
6433
+ if (m.isBackgroundTask) wasBg = true;
6434
+ this.state.messages[i] = this._cancelledCopyOf(m, itemId);
6435
+ changed = true;
6436
+ }
6437
+ }
6438
+ this.cancelledServerIds.delete(itemId);
6439
+ if (!changed) return;
6440
+ if (wasBg) this.promoteNextBgQueuedToRunning();
6441
+ else this.promoteNextQueuedToRunning();
6442
+ this.updateHistoryCache();
6443
+ this.host.notify();
6444
+ }
6445
+ /**
6446
+ * _settleCancelledTurn for a chat that is not on screen: the same edit, made in
6447
+ * its history cache. Matched by id where the cached bubbles carry one; an
6448
+ * off-chat send caches its pair before the ack (see dispatchComposedMessage), so
6449
+ * the fallback is positional, exactly as _applyReplyToCache's is: the trailing
6450
+ * pending placeholder, and the last request bubble still marked in flight.
6451
+ */
6452
+ _applyCancelToCache(key, itemId) {
6453
+ var existing = key ? this.aiChatHistoryCache[key] : void 0;
6454
+ if (!existing) return;
6455
+ var msgs = existing.messages.slice();
6456
+ var changed = false;
6457
+ var pIdx = -1, uIdx = -1;
6458
+ for (var i = msgs.length - 1; i >= 0; i--) {
6459
+ var m = msgs[i];
6460
+ if (!m || m._serverItemId !== itemId) continue;
6461
+ if (m.role === "assistant" && m.isPending) {
6462
+ msgs.splice(i, 1);
6463
+ changed = true;
6464
+ pIdx = i;
6465
+ } else if (m.role === "user" && !m.isCancelled && uIdx === -1) uIdx = i;
6466
+ }
6467
+ if (pIdx === -1) {
6468
+ for (var p = msgs.length - 1; p >= 0; p--) {
6469
+ var pm = msgs[p];
6470
+ if (!pm || !pm.isPending || pm.role !== "assistant" || pm.isBackgroundTask || pm._serverItemId !== void 0) continue;
6471
+ msgs.splice(p, 1);
6472
+ changed = true;
6473
+ break;
6474
+ }
6475
+ }
6476
+ if (uIdx === -1) {
6477
+ for (var u = msgs.length - 1; u >= 0; u--) {
6478
+ var um = msgs[u];
6479
+ if (!um || um.role !== "user" || um.isBackgroundTask || um._serverItemId !== void 0 || um._stageId) continue;
6480
+ if (!(um.isPendingQueued || um.isPendingInProcess || um.isSendingToServer)) continue;
6481
+ uIdx = u;
6482
+ break;
6483
+ }
6484
+ }
6485
+ if (uIdx !== -1) {
6486
+ msgs[uIdx] = this._cancelledCopyOf(msgs[uIdx], itemId);
6487
+ changed = true;
6488
+ }
6489
+ if (!changed) return;
6490
+ this.aiChatHistoryCache[key] = { messages: msgs, endOfList: existing.endOfList, startKeyHistory: existing.startKeyHistory };
6491
+ }
6251
6492
  /**
6252
6493
  * Stop indexing a file, from its collapsed row — every pass at once, not just
6253
6494
  * the bubble the user happens to see.
@@ -6314,6 +6555,7 @@ var ChatSession = class {
6314
6555
  }
6315
6556
  }
6316
6557
  this._adoptWorkerIndexingPasses(0);
6558
+ this._sweepCancelledIndexing();
6317
6559
  var ids = group.cancellableIds || [];
6318
6560
  if (!ids.length) {
6319
6561
  this.host.notify();
@@ -6450,7 +6692,7 @@ var ChatSession = class {
6450
6692
  enqueueTypewrite(idx, fullText, localId, paintedText) {
6451
6693
  var self = this;
6452
6694
  var target = this.state.messages[idx];
6453
- if (target && target._ts === void 0) target._ts = wallClockNow();
6695
+ if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
6454
6696
  if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
6455
6697
  this.typewriterQueue = this.typewriterQueue.then(function() {
6456
6698
  return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
@@ -6596,9 +6838,10 @@ var ChatSession = class {
6596
6838
  this.state.sending = true;
6597
6839
  this.host.scrollToBottomIfSticky(true);
6598
6840
  return Promise.resolve(pending).catch(function() {
6599
- }).then(function() {
6841
+ }).then(function(result) {
6600
6842
  if (token !== self.state.gateRefreshToken) return;
6601
6843
  self.state.sending = false;
6844
+ if (result && result.cancelled) return;
6602
6845
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
6603
6846
  self.host.scrollToBottomIfSticky(true);
6604
6847
  });
@@ -8227,8 +8470,19 @@ function buildChatDisplayList(messages, opts) {
8227
8470
  } else if (stopped) {
8228
8471
  grp.status = "cancelled";
8229
8472
  } else {
8230
- var last = grp.members[grp.members.length - 1].msg;
8231
- grp.status = last.isError ? "error" : "done";
8473
+ var retriedIds = {};
8474
+ for (var ri = 0; ri < grp.members.length; ri++) {
8475
+ var rm = grp.members[ri].msg;
8476
+ if (rm.isRetried && rm._serverItemId) retriedIds[rm._serverItemId] = true;
8477
+ }
8478
+ var li = grp.members.length - 1;
8479
+ while (li >= 0 && (grp.members[li].msg.isRetried || grp.members[li].msg._serverItemId && retriedIds[grp.members[li].msg._serverItemId])) li--;
8480
+ if (li < 0) {
8481
+ grp.status = "active";
8482
+ } else {
8483
+ var last = grp.members[li].msg;
8484
+ grp.status = last.isError ? "error" : "done";
8485
+ }
8232
8486
  }
8233
8487
  var sawFirstPass = false;
8234
8488
  for (var pi = 0; pi < grp.members.length; pi++) {
@@ -8693,6 +8947,7 @@ exports.isOfficeFile = isOfficeFile;
8693
8947
  exports.isPagedReadFile = isPagedReadFile;
8694
8948
  exports.isPreviewableImagePath = isPreviewableImagePath;
8695
8949
  exports.isProviderApiKeyError = isProviderApiKeyError;
8950
+ exports.isRetriedFailure = isRetriedFailure;
8696
8951
  exports.isServerExtractable = isServerExtractable;
8697
8952
  exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
8698
8953
  exports.isWindowedReadFile = isWindowedReadFile;