bunnyquery 1.3.4 → 1.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -352,6 +352,7 @@ export class ChatSession {
352
352
  this.host.notify(); this.enqueueTypewrite(aiIdx, answer, lid);
353
353
  }
354
354
  }
355
+ this._removeStrayPendingAssistants();
355
356
  this.promoteNextQueuedToRunning();
356
357
  this.updateHistoryCache();
357
358
  this.host.notify();
@@ -386,12 +387,14 @@ export class ChatSession {
386
387
  if (thPos2 !== -1) this.state.messages.splice(thPos2, 1);
387
388
  }
388
389
  if (serverId) this.cancelledServerIds.delete(serverId);
390
+ this._removeStrayPendingAssistants();
389
391
  this.promoteNextQueuedToRunning(); this.updateHistoryCache(); this.host.notify(); this.host.scrollToBottom(true);
390
392
  return;
391
393
  }
392
394
  var targetIdx = this.resolveQueuedUserBubble(serverId);
393
395
  if (targetIdx === undefined) { this.host.notify(); this.updateHistoryCache(); return; }
394
396
  this.insertAtTarget({ role: 'assistant', content: getErrorMessage(err), isError: true }, targetIdx);
397
+ this._removeStrayPendingAssistants();
395
398
  this.promoteNextQueuedToRunning(); this.updateHistoryCache(); this.host.notify(); this.host.scrollToBottom(true);
396
399
  }
397
400
 
@@ -516,17 +519,52 @@ export class ChatSession {
516
519
  if (pendingIdx === -1) return Promise.resolve();
517
520
  if (latest.isError || !latest.content) {
518
521
  this.state.messages[pendingIdx] = { role: 'assistant', content: latest.content || '', isError: !!latest.isError };
522
+ this._removeStrayPendingAssistants();
519
523
  this.host.notify();
520
524
  this.promoteNextQueuedToRunning();
521
525
  return Promise.resolve();
522
526
  }
523
527
  var lid = this._newLocalId();
524
528
  this.state.messages[pendingIdx] = { role: 'assistant', content: '', isPending: false, _localId: lid };
529
+ this._removeStrayPendingAssistants();
525
530
  this.host.notify();
526
531
  this.promoteNextQueuedToRunning();
527
532
  return this.enqueueTypewrite(pendingIdx, latest.content, lid);
528
533
  }
529
534
 
535
+ // Remove any leftover non-background pending ("Thinking…") assistant bubbles.
536
+ // There is normally at most ONE such bubble at a time (promoteNext* refuses to
537
+ // add a second), so any extra is a duplicate — it appears when a concurrent
538
+ // history refetch re-maps the still-"running" turn into a pending placeholder
539
+ // (with a real _serverItemId) while the local pending bubble (no _serverItemId)
540
+ // is rescued and re-appended (see loadHistory rescue below). Each resolve path
541
+ // only replaces the FIRST pending bubble, so without this a stray "Thinking…"
542
+ // survives next to the reply/error. MUST run AFTER the resolved bubble has been
543
+ // made non-pending and BEFORE promoteNext*() (so a freshly-promoted Thinking,
544
+ // which is added only once no pending assistant remains, is preserved).
545
+ _removeStrayPendingAssistants(): void {
546
+ for (var k = this.state.messages.length - 1; k >= 0; k--) {
547
+ var m = this.state.messages[k];
548
+ if (m.isPending && m.role === 'assistant' && !m.isBackgroundTask) this.state.messages.splice(k, 1);
549
+ }
550
+ }
551
+
552
+ // Drop the pending flags on the resolved turn's USER bubble (preserving its
553
+ // content + background-task marker). Needed because a bg "Indexing:" turn's user
554
+ // bubble carries isPendingInProcess; leaving it set keeps the bubble visually
555
+ // stuck and keeps its bgTaskQueue entry alive forever.
556
+ _clearPendingUserBubble(itemId: string): void {
557
+ var uIdx = this.state.messages.findIndex(function (m) {
558
+ return m.role === 'user' && m._serverItemId === itemId &&
559
+ (m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
560
+ });
561
+ if (uIdx === -1) return;
562
+ var u = this.state.messages[uIdx];
563
+ var cleaned: ChatMessage = { role: 'user', content: u.content, _serverItemId: itemId };
564
+ if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
565
+ this.state.messages[uIdx] = cleaned;
566
+ }
567
+
530
568
  // If an immediate-send request for the current cache key is still in flight
531
569
  // (e.g. the view unmounted then remounted mid-request), show the sending
532
570
  // state, await it, then render the reply from the cache. Skipped when the
@@ -561,6 +599,11 @@ export class ChatSession {
561
599
  : ((platform === 'openai' ? extractOpenAIText(response) : extractClaudeText(response)) || '').trim();
562
600
  var idx = this.state.messages.findIndex(function (m) { return m.isPending && m._serverItemId === itemId; });
563
601
  if (idx !== -1) {
602
+ // A bg "Indexing:" turn pushes a user bubble (isPendingInProcess) ALONGSIDE
603
+ // the assistant Thinking; replacing only the assistant leaves that user
604
+ // bubble stuck pending — and drainBgTaskQueue then never clears its queue
605
+ // entry (its stillPending check stays true). Un-pend it here too.
606
+ this._clearPendingUserBubble(itemId);
564
607
  if (isErr) {
565
608
  this.state.messages[idx] = { role: 'assistant', content: answer, isError: true, _serverItemId: itemId };
566
609
  this.host.notify(); this.updateHistoryCache(); return;
@@ -690,12 +733,23 @@ export class ChatSession {
690
733
  mapped.forEach(function (m: any) { if (m._serverItemId) serverIds[m._serverItemId] = 1; });
691
734
  var locallyCancelled: any = {};
692
735
  self.state.messages.forEach(function (m) { if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1; });
736
+ // If the freshly-mapped server list ALREADY shows this in-flight turn
737
+ // as a pending placeholder (a non-bg pending assistant, which carries
738
+ // a real _serverItemId), re-pushing the local no-_serverItemId
739
+ // user+Thinking would DUPLICATE it. Each resolve path only clears the
740
+ // first pending bubble, so the duplicate strands a "Thinking…" beside
741
+ // the reply/error. There is at most one in-flight regular turn, so a
742
+ // mapped pending assistant IS this turn — skip the redundant local copy.
743
+ var mappedHasPendingAssistant = mapped.some(function (m: any) {
744
+ return m.isPending && m.role === 'assistant' && !m.isBackgroundTask;
745
+ });
693
746
  var rescued: ChatMessage[] = [];
694
747
  for (var ri = 0; ri < self.state.messages.length; ri++) {
695
748
  var mm = self.state.messages[ri];
696
749
  if (mm.isBackgroundTask) continue;
697
750
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
698
751
  if (!mm._serverItemId) {
752
+ if (mappedHasPendingAssistant) continue;
699
753
  if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
700
754
  else if (self.state.sending && mm.role === 'user') {
701
755
  var next = self.state.messages[ri + 1];