bunnyquery 1.4.3 → 1.4.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.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -105,11 +105,23 @@ export class ChatSession {
105
105
 
106
106
  dispatchAgentRequest(params: any) {
107
107
  var self = this;
108
+ // Id of the in-flight item this dispatch polls. Recorded in
109
+ // historyItemPolls (set below, deleted when the dispatch settles) so a
110
+ // remount / history refetch dedups against THIS poll instead of stacking
111
+ // a duplicate history poll on the same item. (The cacheKey guard alone
112
+ // stops covering an item once pendingAgentRequests clears — e.g. a queued
113
+ // message still in flight after the immediate one resolved.)
114
+ var dispatchItemId: string | undefined;
108
115
  var sendAndPoll = function () {
109
116
  return Promise.resolve(
110
117
  self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent)
111
118
  ).then(function (initial: any) {
112
119
  if (initial && initial.poll && (initial.status === 'pending' || initial.status === 'running')) {
120
+ if (initial.id) {
121
+ if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
122
+ dispatchItemId = initial.id;
123
+ self.historyItemPolls.set(initial.id, true);
124
+ }
113
125
  return initial.poll({ latency: POLL_INTERVAL });
114
126
  }
115
127
  return initial;
@@ -138,19 +150,50 @@ export class ChatSession {
138
150
  })
139
151
  .catch(function (err: any) { return { content: getErrorMessage(err), isError: true }; })
140
152
  .then(function (result: any) {
141
- // Append the resolved reply to the shared history cache. This runs
142
- // independently of the view lifecycle, so a reply lands in the cache
143
- // even if the view unmounted mid-request; the view renders it from
144
- // the cache via typewriteLatestReply (and resumePendingRequest on
145
- // remount). The displayed bubble in state.messages is updated by the
146
- // caller's typewriteLatestReply, not here.
147
- var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
148
- self.aiChatHistoryCache[params.key] = {
149
- messages: existing.messages.concat([{ role: 'assistant', content: result.content, isError: result.isError }]),
150
- endOfList: existing.endOfList,
151
- startKeyHistory: existing.startKeyHistory,
152
- };
153
+ // Land the resolved reply in the shared history cache. This runs
154
+ // independently of the view lifecycle, so a reply is captured even
155
+ // if the chatbox unmounted mid-request.
153
156
  delete self.pendingAgentRequests[params.key];
157
+ if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
158
+ var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
159
+ var reply: ChatMessage = { role: 'assistant', content: result.content, isError: result.isError };
160
+
161
+ var onThisVisibleChat = self.host.isViewMounted() && self.getHistoryCacheKey() === params.key;
162
+ if (onThisVisibleChat) {
163
+ // The chatbox is showing THIS chat: its typewriteLatestReply will
164
+ // swap the "Thinking..." bubble in state.messages and re-snapshot
165
+ // the cache. Just append the reply for it to pick up.
166
+ self.aiChatHistoryCache[params.key] = {
167
+ messages: existing.messages.concat([reply]),
168
+ endOfList: existing.endOfList,
169
+ startKeyHistory: existing.startKeyHistory,
170
+ };
171
+ } else {
172
+ // Resolved while this chat is NOT the visible view (the chatbox
173
+ // unmounted, or the user moved to another project). No view
174
+ // typewriter will run, so REPLACE the trailing pending
175
+ // "Thinking..." bubble in the cache with the answer rather than
176
+ // appending a duplicate — otherwise a remount renders a stuck
177
+ // "Thinking..." (whose bubble was never resolved). The next
178
+ // fresh history fetch reconciles it either way.
179
+ var msgs = existing.messages.slice();
180
+ var idx = -1;
181
+ for (var i = msgs.length - 1; i >= 0; i--) {
182
+ var m = msgs[i];
183
+ if (m && m.isPending && m.role === 'assistant' && !m.isBackgroundTask) { idx = i; break; }
184
+ }
185
+ if (idx !== -1) {
186
+ reply._serverItemId = msgs[idx]._serverItemId;
187
+ msgs[idx] = reply;
188
+ } else {
189
+ msgs.push(reply);
190
+ }
191
+ self.aiChatHistoryCache[params.key] = {
192
+ messages: msgs,
193
+ endOfList: existing.endOfList,
194
+ startKeyHistory: existing.startKeyHistory,
195
+ };
196
+ }
154
197
  return result;
155
198
  });
156
199
  this.pendingAgentRequests[params.key] = run;
@@ -205,6 +248,9 @@ export class ChatSession {
205
248
  self.state.messages[sendingIdx] = upd; self.host.notify();
206
249
  }
207
250
  if (result && result.poll && (result.status === 'pending' || result.status === 'running')) {
251
+ // Track this queued item's poll so a remount/refetch dedups
252
+ // against it instead of attaching a duplicate history poll.
253
+ if (serverId) self.historyItemPolls.set(serverId, true);
208
254
  return result.poll({ latency: POLL_INTERVAL })
209
255
  .then(function (res: any) { return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId); })
210
256
  .catch(function (err: any) { return self.onQueuedSendError(capturedComposed, err, serverId); });
@@ -238,14 +284,23 @@ export class ChatSession {
238
284
  platform: aiPlatform, model: aiModel, systemPrompt: systemPrompt, serviceId: id.serviceId,
239
285
  history: historyForLlm,
240
286
  });
241
- var requestToken = this.state.gateRefreshToken;
242
287
  var run = this.dispatchAgentRequest({
243
288
  key: key, serviceId: id.serviceId, owner: id.owner, aiPlatform: aiPlatform, aiModel: aiModel,
244
289
  systemPrompt: systemPrompt, text: composed, boundedMessages: bounded.messages, userId: chatQueue,
245
290
  extractContent: extractContent,
246
291
  });
292
+ // Render the reply into the "Thinking..." bubble whenever the chatbox is
293
+ // CURRENTLY showing this chat — even after an unmount/remount. The old
294
+ // guard bailed on `requestToken !== gateRefreshToken`, but EVERY remount
295
+ // bumps gateRefreshToken (refreshGate), so a request that finished after
296
+ // the user navigated away and back left a stuck "Thinking..." (no view
297
+ // typewriter ever ran, and resumePendingRequest bails when a pending
298
+ // bubble is already present). Gate on "is this the visible chat" instead.
299
+ // When it ISN'T (unmounted / another project), dispatchAgentRequest has
300
+ // already replaced the pending bubble with the answer IN THE CACHE, so a
301
+ // later loadChatHistory renders it.
247
302
  Promise.resolve(run).catch(function () { }).then(function () {
248
- if (requestToken !== self.state.gateRefreshToken || self.getHistoryCacheKey() !== key) return;
303
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
249
304
  self.state.sending = false;
250
305
  return Promise.resolve(self.typewriteLatestReply(key)).then(function () { self.host.scrollToBottom(true); });
251
306
  });
@@ -333,6 +388,7 @@ export class ChatSession {
333
388
  }
334
389
 
335
390
  onQueuedSendResponse(_composed: string, response: any, platform: string, serverId?: string): void {
391
+ if (serverId) this.historyItemPolls.delete(serverId);
336
392
  var targetIdx = this.resolveQueuedUserBubble(serverId);
337
393
  if (targetIdx === undefined) { this.host.notify(); this.updateHistoryCache(); return; }
338
394
  if (isErrorResponseBody(response)) {
@@ -362,6 +418,7 @@ export class ChatSession {
362
418
 
363
419
  onQueuedSendError(_composed: string, err: any, serverId?: string): void {
364
420
  var self = this;
421
+ if (serverId) this.historyItemPolls.delete(serverId);
365
422
  var isNotExists = err && (err.code === 'NOT_EXISTS' || (err.body && err.body.code === 'NOT_EXISTS'));
366
423
  if (isNotExists) {
367
424
  var userIdx = serverId
@@ -789,7 +846,11 @@ export class ChatSession {
789
846
  if (item.status !== 'running' && item.status !== 'pending') return;
790
847
  if (!item.poll || !item.id) return;
791
848
  if (self.historyItemPolls.has(item.id)) return;
792
- if (item.status === 'running' && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
849
+ // running OR pending: a live dispatchAgentRequest already polls this
850
+ // item; attaching a second (history) poll would double the interval
851
+ // fetch on a remount (skapi item.poll() loops survive unmount —
852
+ // they're uncancellable, so the dispatch poll is still running).
853
+ if ((item.status === 'running' || item.status === 'pending') && self.pendingAgentRequests[self.getHistoryCacheKey()]) return;
793
854
  self.historyItemPolls.set(item.id, true);
794
855
  var capturedId = item.id;
795
856
  var pp = item.poll({