bunnyquery 1.8.13 → 1.8.14

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/bunnyquery.js CHANGED
@@ -857,18 +857,31 @@ Index the REMAINING windows - one record per row/item, looking at any page image
857
857
  part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
858
858
  });
859
859
  }
860
+ function canonicalizePathForm(value) {
861
+ if (!value) return value;
862
+ try {
863
+ return value.normalize("NFC");
864
+ } catch (e) {
865
+ return value;
866
+ }
867
+ }
860
868
  function linkUnavailableKeyForPath(remotePath) {
861
- return "path:" + (remotePath || "");
869
+ return "path:" + canonicalizePathForm(remotePath || "");
862
870
  }
863
871
  function linkUnavailableKeyForHref(href) {
864
- return "href:" + (href || "");
872
+ var carried = readExpiredAttachmentHref(href);
873
+ if (carried) return linkUnavailableKeyForPath(carried);
874
+ return "href:" + canonicalizePathForm(href || "");
865
875
  }
866
876
  function linkUnavailableKeysForPath(remotePath) {
867
877
  if (!remotePath) return [];
868
- return [
878
+ var keys = [
869
879
  linkUnavailableKeyForPath(remotePath),
870
880
  linkUnavailableKeyForHref(buildDisplayExpiredAttachmentHref(remotePath))
871
881
  ];
882
+ return keys.filter(function(k, i) {
883
+ return keys.indexOf(k) === i;
884
+ });
872
885
  }
873
886
  function isLinkUnavailable(link, map) {
874
887
  if (!link || !map) return false;
@@ -1247,7 +1260,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1247
1260
  if (link.remotePath) attrs.push('data-bq-remote-path="' + escapeInlineHtml(link.remotePath) + '"');
1248
1261
  if (link.fullLabel) attrs.push('data-bq-full-label="' + escapeInlineHtml(link.fullLabel) + '"');
1249
1262
  if (!preview) return "<a " + attrs.join(" ") + ">" + escapeInlineHtml(labelText) + "</a>";
1250
- return "<a " + attrs.join(" ") + '><img class="bq-img-preview" alt="' + escapeInlineHtml(full) + '" data-bq-img-path="' + escapeInlineHtml(link.remotePath || "") + '" data-bq-img-type="' + escapeInlineHtml(link.image ? link.image.contentType : "") + '" loading="lazy" decoding="async"><span class="bq-loader" data-bq-img-loader="1"></span><span class="bq-img-preview-caption" translate="no">' + escapeInlineHtml(labelText) + "</span></a>";
1263
+ return "<a " + attrs.join(" ") + '><img class="bq-img-preview" alt="' + escapeInlineHtml(full) + '" data-bq-img-path="' + escapeInlineHtml(link.remotePath || "") + '" data-bq-img-type="' + escapeInlineHtml(link.image ? link.image.contentType : "") + '" decoding="async"><span class="bq-loader" data-bq-img-loader="1"></span><span class="bq-img-preview-caption" translate="no">' + escapeInlineHtml(labelText) + "</span></a>";
1251
1264
  }
1252
1265
 
1253
1266
  // src/engine/image_preview.ts
@@ -1327,31 +1340,48 @@ Index the REMAINING windows - one record per row/item, looking at any page image
1327
1340
  var warm = peekImagePreviewUrl(ctx, path);
1328
1341
  if (warm) {
1329
1342
  img.setAttribute("src", warm);
1343
+ notifyLayoutChange(img, ctx, path);
1330
1344
  return;
1331
1345
  }
1332
1346
  resolveImagePreviewUrl(ctx, path, type).then(function(url) {
1333
1347
  if (img.getAttribute("data-bq-img-state") !== "loading") return;
1334
1348
  img.setAttribute("src", url);
1349
+ notifyLayoutChange(img, ctx, path);
1335
1350
  }, function(e) {
1336
1351
  img.setAttribute("data-bq-img-state", "error");
1352
+ notifyLayoutChange(img, ctx, path);
1337
1353
  if (ctx.onError) ctx.onError(path, e);
1338
1354
  });
1339
1355
  }
1340
1356
  function onImageError(img, ctx, path, type) {
1341
1357
  if (img.getAttribute("data-bq-img-retry") === "1") {
1342
1358
  img.setAttribute("data-bq-img-state", "error");
1359
+ notifyLayoutChange(img, ctx, path);
1343
1360
  if (ctx.onError) ctx.onError(path, new Error("image preview failed to load"));
1344
1361
  return;
1345
1362
  }
1346
1363
  img.setAttribute("data-bq-img-retry", "1");
1347
1364
  img.removeAttribute("src");
1365
+ notifyLayoutChange(img, ctx, path);
1348
1366
  resolveImagePreviewUrl(ctx, path, type, true).then(function(url) {
1349
1367
  img.setAttribute("src", url);
1368
+ notifyLayoutChange(img, ctx, path);
1350
1369
  }, function(e) {
1351
1370
  img.setAttribute("data-bq-img-state", "error");
1371
+ notifyLayoutChange(img, ctx, path);
1352
1372
  if (ctx.onError) ctx.onError(path, e);
1353
1373
  });
1354
1374
  }
1375
+ function notifyLayoutChange(img, ctx, path) {
1376
+ if (!ctx.onLayoutChange) return;
1377
+ ctx.onLayoutChange(previewLayoutBox(img), path);
1378
+ }
1379
+ var PREVIEW_LAYOUT_BOX_SELECTOR = "a.bq-link-button.is-image-preview";
1380
+ function previewLayoutBox(img) {
1381
+ var closest = img.closest;
1382
+ if (typeof closest !== "function") return img;
1383
+ return closest.call(img, PREVIEW_LAYOUT_BOX_SELECTOR) || img;
1384
+ }
1355
1385
 
1356
1386
  // src/engine/time.ts
1357
1387
  function wallClockNow() {
@@ -2389,6 +2419,169 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2389
2419
  }
2390
2420
  return { messages: mapped, runningItemIds };
2391
2421
  }
2422
+ function shouldRescueInFlightMessage(m, ctx) {
2423
+ if (!m) return false;
2424
+ if (m.isBackgroundTask) return false;
2425
+ if (m._ownerKey !== void 0 && ctx.loadKey !== void 0 && m._ownerKey !== ctx.loadKey) return false;
2426
+ if (m._serverItemId && ctx.hasServerId(m._serverItemId)) return false;
2427
+ if (m._stageId) return true;
2428
+ if (!m._serverItemId && ctx.pageHasPendingAssistant) return false;
2429
+ if (m.isSendingToServer || m.isPendingQueued || m.isPendingInProcess || m.isPending) return true;
2430
+ if (ctx.sending && m.role === "user") {
2431
+ var next = ctx.next;
2432
+ if (!next || next.isBackgroundTask || !next.isPending) return false;
2433
+ return next._serverItemId === void 0 || next._serverItemId === m._serverItemId;
2434
+ }
2435
+ return false;
2436
+ }
2437
+
2438
+ // src/engine/scroll_anchor.ts
2439
+ var ROW_KEY_ATTR = "data-row-key";
2440
+ var ROW_POS_ATTR = "data-row-pos";
2441
+ var UNKNOWN_ROW_POS = "\0?";
2442
+ function createScrollAnchor(options) {
2443
+ var held = null;
2444
+ var seen = typeof WeakMap === "function" ? /* @__PURE__ */ new WeakMap() : null;
2445
+ function capture() {
2446
+ var box = options.getBox();
2447
+ if (!box || options.isStuck()) return null;
2448
+ var boxTop = box.getBoundingClientRect().top;
2449
+ var kids = box.children;
2450
+ var fallback = null;
2451
+ for (var i = 0; i < kids.length; i++) {
2452
+ var el = kids[i];
2453
+ if (!el || typeof el.getAttribute !== "function") continue;
2454
+ var key = el.getAttribute(ROW_KEY_ATTR);
2455
+ if (!key) continue;
2456
+ var top = el.getBoundingClientRect().top - boxTop;
2457
+ if (top + el.offsetHeight <= 0) continue;
2458
+ if (top >= box.clientHeight) break;
2459
+ var rawPos = el.getAttribute(ROW_POS_ATTR);
2460
+ var pos = rawPos === null ? null : rawPos || UNKNOWN_ROW_POS;
2461
+ var cand = {
2462
+ key,
2463
+ top,
2464
+ pos,
2465
+ scrollTop: box.scrollTop,
2466
+ scrollHeight: box.scrollHeight,
2467
+ el
2468
+ };
2469
+ if (rawPos === null) return cand;
2470
+ if (!fallback) fallback = cand;
2471
+ }
2472
+ return fallback || {
2473
+ key: null,
2474
+ top: 0,
2475
+ pos: null,
2476
+ scrollTop: box.scrollTop,
2477
+ scrollHeight: box.scrollHeight,
2478
+ el: null
2479
+ };
2480
+ }
2481
+ function findRow(box, anchor) {
2482
+ var el = anchor.el;
2483
+ if (el && el.parentNode === box) return el;
2484
+ if (!anchor.key) return null;
2485
+ var kids = box.children;
2486
+ for (var i = 0; i < kids.length; i++) {
2487
+ var kid = kids[i];
2488
+ if (!kid || typeof kid.getAttribute !== "function") continue;
2489
+ if (kid.getAttribute(ROW_KEY_ATTR) === anchor.key) return kid;
2490
+ }
2491
+ return null;
2492
+ }
2493
+ function restore(anchor) {
2494
+ var box = options.getBox();
2495
+ if (!box || !anchor || options.isStuck()) return;
2496
+ var el = findRow(box, anchor);
2497
+ if (el) {
2498
+ var livePos = el.getAttribute(ROW_POS_ATTR) || UNKNOWN_ROW_POS;
2499
+ if (anchor.pos !== null && anchor.pos !== UNKNOWN_ROW_POS && livePos !== UNKNOWN_ROW_POS && livePos !== anchor.pos) {
2500
+ lost(box, anchor);
2501
+ return;
2502
+ }
2503
+ var boxTop = box.getBoundingClientRect().top;
2504
+ var delta = el.getBoundingClientRect().top - boxTop - anchor.top;
2505
+ var slack = Math.abs(box.scrollHeight - anchor.scrollHeight) + box.clientHeight;
2506
+ if (delta > slack || delta < -slack) {
2507
+ lost(box, anchor);
2508
+ return;
2509
+ }
2510
+ if (delta >= 1 || delta <= -1) box.scrollTop += delta;
2511
+ held = {
2512
+ key: anchor.key,
2513
+ top: anchor.top,
2514
+ pos: anchor.pos,
2515
+ scrollTop: box.scrollTop,
2516
+ scrollHeight: box.scrollHeight,
2517
+ el
2518
+ };
2519
+ return;
2520
+ }
2521
+ lost(box, anchor);
2522
+ }
2523
+ function lost(box, anchor) {
2524
+ held = null;
2525
+ var grew = box.scrollHeight - anchor.scrollHeight;
2526
+ if (grew > 0) {
2527
+ box.scrollTop = anchor.scrollTop + grew;
2528
+ return;
2529
+ }
2530
+ if (options.rawFallback) box.scrollTop = anchor.scrollTop;
2531
+ }
2532
+ function preserve(mutate) {
2533
+ var anchor = capture();
2534
+ var result = mutate();
2535
+ restore(anchor);
2536
+ return result;
2537
+ }
2538
+ function remember() {
2539
+ held = capture();
2540
+ }
2541
+ function hold() {
2542
+ var box = options.getBox();
2543
+ if (!box || options.isStuck()) {
2544
+ held = null;
2545
+ return;
2546
+ }
2547
+ if (!held) {
2548
+ held = capture();
2549
+ return;
2550
+ }
2551
+ if (box.scrollTop !== held.scrollTop) {
2552
+ held = capture();
2553
+ return;
2554
+ }
2555
+ restore(held);
2556
+ }
2557
+ function absorb(el) {
2558
+ if (!el) return;
2559
+ var box = options.getBox();
2560
+ if (!box) return;
2561
+ var h = el.offsetHeight;
2562
+ var prev = seen ? seen.get(el) : void 0;
2563
+ if (seen) seen.set(el, h);
2564
+ if (options.isStuck()) return;
2565
+ if (prev === void 0) prev = 0;
2566
+ var delta = h - prev;
2567
+ if (delta === 0) return;
2568
+ if (el.getBoundingClientRect().top >= box.getBoundingClientRect().top) return;
2569
+ box.scrollTop += delta;
2570
+ if (held) held = capture();
2571
+ }
2572
+ function forget() {
2573
+ held = null;
2574
+ }
2575
+ return {
2576
+ capture,
2577
+ restore,
2578
+ preserve,
2579
+ remember,
2580
+ hold,
2581
+ absorb,
2582
+ forget
2583
+ };
2584
+ }
2392
2585
 
2393
2586
  // src/engine/viewport_fill.ts
2394
2587
  var HISTORY_FILL_SLACK_PX = 64;
@@ -2500,6 +2693,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2500
2693
  var LIVE_INDEX_SNAPSHOT_MAX_AGE_MS = 5e3;
2501
2694
  var INDEX_DISPATCH_CLAIM_MS = 2 * 60 * 1e3;
2502
2695
  var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
2696
+ var EARLY_PROBE_SCHEDULE_MS = [400, 900, 1700];
2503
2697
  var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
2504
2698
  var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
2505
2699
  var INDEXING_DRAIN_IDLE_LOOKS = 2;
@@ -2895,7 +3089,93 @@ Index the REMAINING windows - one record per row/item, looking at any page image
2895
3089
  *
2896
3090
  * `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
2897
3091
  * poll simply cannot be stopped and is left running — see pausePolling.
3092
+ *
3093
+ * (This block documents _trackPoll, further down. The two methods below sit between it
3094
+ * and its subject.)
3095
+ */
3096
+ /**
3097
+ * Foreground poll with an early-probe race.
3098
+ *
3099
+ * skapi's poll() is a bare setInterval(fn, latency) with NO check at t=0, so the earliest a
3100
+ * reply can be observed is one full POLL_INTERVAL (3s) after dispatch. For a long generation
3101
+ * that granularity is free. For a SHORT one it is nearly pure dead time: a greeting that the
3102
+ * provider finishes in 1s still waits until the 3s tick, which measured as a large share of a
3103
+ * 5s "yo" round trip.
3104
+ *
3105
+ * So keep the 3s interval as the steady state, and additionally point-look-up the item a few
3106
+ * times early, on a widening schedule. Whichever answers first wins and the other is stopped.
3107
+ * The probe uses the csrHistoryItemLookup hook both clients already implement; without it this
3108
+ * degrades to exactly the old behaviour.
3109
+ *
3110
+ * FOREGROUND ONLY. Background indexing polls keep the flat cadence: nobody is watching them,
3111
+ * and they are the ones bounded by MAX_CONCURRENT_BG_POLLS, so adding probes there would spend
3112
+ * the request budget the cap exists to protect.
2898
3113
  */
3114
+ attachForegroundPoll(source, itemId, opts) {
3115
+ return this._fgPollWithEarlyProbe(source, itemId, opts);
3116
+ }
3117
+ _fgPollWithEarlyProbe(source, itemId, opts) {
3118
+ var base = source.poll(Object.assign({ latency: POLL_INTERVAL }, opts || {}));
3119
+ var lookup = chatEngineConfig().csrHistoryItemLookup;
3120
+ var ident = this.host.getIdentity();
3121
+ var platform = ident && ident.platform;
3122
+ if (!lookup || !itemId || !ident || !ident.projectId || platform !== "claude" && platform !== "openai") {
3123
+ return base;
3124
+ }
3125
+ var settled = false;
3126
+ var timers = [];
3127
+ var clearProbes = function() {
3128
+ for (var i = 0; i < timers.length; i++) clearTimeout(timers[i]);
3129
+ timers = [];
3130
+ };
3131
+ var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
3132
+ var raced = new Promise(function(resolve, reject) {
3133
+ base.then(function(res) {
3134
+ if (settled) return;
3135
+ settled = true;
3136
+ clearProbes();
3137
+ resolve(res);
3138
+ }, function(err) {
3139
+ if (settled) return;
3140
+ settled = true;
3141
+ clearProbes();
3142
+ reject(err);
3143
+ });
3144
+ var fullId = buildHistoryItemFullId(platform, ident.projectId, itemId);
3145
+ EARLY_PROBE_SCHEDULE_MS.forEach(function(delay) {
3146
+ timers.push(setTimeout(function() {
3147
+ if (settled) return;
3148
+ Promise.resolve(lookup(fullId, ident.projectId, ident.owner)).then(function(body) {
3149
+ if (settled || !body || typeof body !== "object") return;
3150
+ if (body.status === "pending" || body.status === "running") return;
3151
+ if (isPollStopped(body)) return;
3152
+ settled = true;
3153
+ clearProbes();
3154
+ if (stopBase) {
3155
+ try {
3156
+ stopBase();
3157
+ } catch (e) {
3158
+ }
3159
+ }
3160
+ if (opts && typeof opts.onResponse === "function") {
3161
+ try {
3162
+ opts.onResponse(body);
3163
+ } catch (e) {
3164
+ }
3165
+ }
3166
+ resolve(body);
3167
+ }, function() {
3168
+ });
3169
+ }, delay));
3170
+ });
3171
+ });
3172
+ raced.stop = function() {
3173
+ settled = true;
3174
+ clearProbes();
3175
+ if (stopBase) stopBase();
3176
+ };
3177
+ return raced;
3178
+ }
2899
3179
  _trackPoll(id, kind, p) {
2900
3180
  var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
2901
3181
  if (!stop) {
@@ -3071,6 +3351,68 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3071
3351
  startKeyHistory: this.state.historyStartKeyHistory.slice()
3072
3352
  };
3073
3353
  }
3354
+ /**
3355
+ * Give the immediate-send pair the server's id for their turn, the moment the
3356
+ * dispatch learns it.
3357
+ *
3358
+ * WHY THIS EXISTS. An immediate send pushes its user bubble and its
3359
+ * "Thinking..." placeholder locally, and until now neither ever carried a
3360
+ * _serverItemId — only the QUEUED path stamped one, off its ack. So for the
3361
+ * whole life of the turn there was no way to tell the local copy and the
3362
+ * server's copy of the SAME turn apart, and the history merge fell back to a
3363
+ * heuristic: rescue the local pair unless the freshly-fetched page happens to
3364
+ * contain a pending assistant.
3365
+ *
3366
+ * That heuristic has a hole exactly one poll interval wide. The server settles
3367
+ * the request; for up to POLL_INTERVAL the client has not noticed, so
3368
+ * `state.sending` is still true and the local pair is still on screen — while a
3369
+ * history fetch issued in that window returns the turn ALREADY SETTLED, with no
3370
+ * pending assistant in it. The rescue then re-appends the local pair below the
3371
+ * server's copy (the question, twice), and when the poll finally resolves,
3372
+ * typewriteLatestReply writes the answer into the rescued placeholder because it
3373
+ * is the only pending assistant left (the answer, twice). updateHistoryCache
3374
+ * persists the result, so it survives every later visit.
3375
+ *
3376
+ * Navigating away while waiting and coming back is what lands a fetch in that
3377
+ * window: a remount runs refreshGate -> a fresh first page, at an arbitrary
3378
+ * moment relative to the 3s poll.
3379
+ *
3380
+ * With the id on the bubbles, both clients' rescue loops skip them through the
3381
+ * dedup they already have (`_serverItemId is in this page`), the reply the
3382
+ * dispatch caches inherits the id too, and nothing needs a new special case.
3383
+ *
3384
+ * Matched by _localId, never by index: a file's indexing rows are spliced in
3385
+ * above these bubbles while the request is in flight.
3386
+ */
3387
+ _stampTurnWithItemId(key, userLid, placeholderLid, itemId) {
3388
+ if (!itemId) return;
3389
+ var changed = false;
3390
+ var stamp = function(list) {
3391
+ var hit = false;
3392
+ for (var i = 0; i < list.length; i++) {
3393
+ var m = list[i];
3394
+ if (!m || !m._localId) continue;
3395
+ if (m._localId !== userLid && m._localId !== placeholderLid) continue;
3396
+ if (m._serverItemId === itemId) continue;
3397
+ list[i] = Object.assign({}, m, { _serverItemId: itemId });
3398
+ hit = true;
3399
+ }
3400
+ return hit;
3401
+ };
3402
+ changed = stamp(this.state.messages);
3403
+ var cached = key ? this.aiChatHistoryCache[key] : void 0;
3404
+ if (cached) {
3405
+ var msgs = cached.messages.slice();
3406
+ if (stamp(msgs)) {
3407
+ this.aiChatHistoryCache[key] = {
3408
+ messages: msgs,
3409
+ endOfList: cached.endOfList,
3410
+ startKeyHistory: cached.startKeyHistory
3411
+ };
3412
+ }
3413
+ }
3414
+ if (changed) this.host.notify();
3415
+ }
3074
3416
  /**
3075
3417
  * Land a resolved reply in the history cache of a chat that is NOT currently
3076
3418
  * visible, without touching state.messages. Mirrors the cache-only path in
@@ -3152,8 +3494,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3152
3494
  if (initial.id) {
3153
3495
  if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
3154
3496
  dispatchItemId = initial.id;
3497
+ if (typeof params.onItemId === "function") params.onItemId(initial.id);
3155
3498
  }
3156
- var dp = initial.poll({ latency: POLL_INTERVAL });
3499
+ var dp = self._fgPollWithEarlyProbe(initial, initial.id);
3157
3500
  if (initial.id) self._trackPoll(initial.id, "fg", dp);
3158
3501
  return dp;
3159
3502
  }
@@ -3178,28 +3521,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3178
3521
  }).then(function(result) {
3179
3522
  delete self.pendingAgentRequests[params.key];
3180
3523
  if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
3181
- var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
3182
3524
  var reply = { role: "assistant", content: result.content, isError: result.isError };
3183
- var msgs = existing.messages.slice();
3184
- var idx = -1;
3185
- for (var i = msgs.length - 1; i >= 0; i--) {
3186
- var m = msgs[i];
3187
- if (m && m.isPending && m.role === "assistant" && !m.isBackgroundTask) {
3188
- idx = i;
3189
- break;
3190
- }
3191
- }
3192
- if (idx !== -1) {
3193
- reply._serverItemId = msgs[idx]._serverItemId;
3194
- msgs[idx] = reply;
3195
- } else {
3196
- msgs.push(reply);
3197
- }
3198
- self.aiChatHistoryCache[params.key] = {
3199
- messages: msgs,
3200
- endOfList: existing.endOfList,
3201
- startKeyHistory: existing.startKeyHistory
3202
- };
3525
+ if (dispatchItemId) reply._serverItemId = dispatchItemId;
3526
+ self._applyReplyToCache(params.key, reply, dispatchItemId);
3203
3527
  return result;
3204
3528
  });
3205
3529
  this.pendingAgentRequests[params.key] = run;
@@ -3588,8 +3912,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3588
3912
  }
3589
3913
  this.host.notify();
3590
3914
  this.updateHistoryCache();
3591
- this.host.scrollToBottom(true);
3915
+ this.scrollForDispatch(stageId);
3592
3916
  var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
3917
+ var capturedQueuedLid = queuedBubble._localId;
3593
3918
  Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.projectId, id.owner)).then(function(result) {
3594
3919
  var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
3595
3920
  return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
@@ -3601,8 +3926,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3601
3926
  self.state.messages[sendingIdx] = upd;
3602
3927
  self.host.notify();
3603
3928
  }
3929
+ if (serverId) self._stampTurnWithItemId(capturedKey, capturedQueuedLid, void 0, serverId);
3604
3930
  if (result && result.poll && (result.status === "pending" || result.status === "running")) {
3605
- var qp = result.poll({ latency: POLL_INTERVAL });
3931
+ var qp = self._fgPollWithEarlyProbe(result, serverId);
3606
3932
  if (serverId) self._trackPoll(serverId, "fg", qp);
3607
3933
  return qp.then(function(res) {
3608
3934
  if (isPollStopped(res)) return;
@@ -3618,7 +3944,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3618
3944
  return;
3619
3945
  }
3620
3946
  var immediateUser = { role: "user", content: composed, _localId: this._newLocalId(), _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
3621
- var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
3947
+ var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _localId: this._newLocalId(), ...key ? { _ownerKey: key } : {} };
3622
3948
  var iStage = this._stageIndex(this.state.messages, stageId);
3623
3949
  if (iStage !== -1) {
3624
3950
  var iEx = this.state.messages[iStage];
@@ -3632,7 +3958,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3632
3958
  this.host.notify();
3633
3959
  this.updateHistoryCache();
3634
3960
  this.state.sending = true;
3635
- this.host.scrollToBottom(true);
3961
+ this.scrollForDispatch(stageId);
3636
3962
  var historyForLlm = this.state.messages.filter(function(m) {
3637
3963
  if (m === immediateUser) return false;
3638
3964
  return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
@@ -3645,6 +3971,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3645
3971
  projectId: id.projectId,
3646
3972
  history: historyForLlm
3647
3973
  });
3974
+ var immediateUserLid = immediateUser._localId, immediatePlaceholderLid = immediatePlaceholder._localId;
3648
3975
  var run = this.dispatchAgentRequest({
3649
3976
  key,
3650
3977
  projectId: id.projectId,
@@ -3656,7 +3983,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3656
3983
  boundedMessages: bounded.messages,
3657
3984
  userId: chatQueue,
3658
3985
  extractContent,
3659
- fileUrls
3986
+ fileUrls,
3987
+ // THE fix for a turn rendering twice after navigate-away-and-back. Until
3988
+ // this existed the immediate-send pair carried no server id for its whole
3989
+ // life (only the QUEUED path stamped one, off its ack), so nothing could
3990
+ // tell the local copy and the server's copy of the same turn apart. See
3991
+ // _stampTurnWithItemId.
3992
+ onItemId: function(itemId) {
3993
+ self._stampTurnWithItemId(key, immediateUserLid, immediatePlaceholderLid, itemId);
3994
+ }
3660
3995
  });
3661
3996
  Promise.resolve(run).catch(function() {
3662
3997
  }).then(function() {
@@ -3667,6 +4002,28 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3667
4002
  });
3668
4003
  });
3669
4004
  }
4005
+ /**
4006
+ * Scroll for a dispatch that is going out NOW, but never for one arriving late.
4007
+ *
4008
+ * A turn with attachments does not dispatch when the user hits Send: it waits out
4009
+ * its uploads and then its whole indexing chain, which is minutes
4010
+ * (awaitIndexingDrained). By then the reader has very often scrolled up into
4011
+ * history to pass the time, and the forcing scrollToBottom yanked them out of it
4012
+ * — and worse, force-pinned stickToBottom, which no-ops every method on the
4013
+ * scroll anchor and re-arms the queue-detect poll whose only bail is
4014
+ * !stickToBottom.
4015
+ *
4016
+ * `stageId` is the exact marker for that case: only the attachment path ever
4017
+ * produces one. The gesture itself was already paid for at stage time, where
4018
+ * stageOutgoingMessage forces the scroll while the user is still looking at the
4019
+ * composer. Deliberately NOT gated on "did we find the staged bubble" — a remount
4020
+ * rebuilds from the cache and the turn is appended rather than replaced, which is
4021
+ * just as late and just as unrequested.
4022
+ */
4023
+ scrollForDispatch(stageId) {
4024
+ if (stageId) this.host.scrollToBottomIfSticky(true);
4025
+ else this.host.scrollToBottom(true);
4026
+ }
3670
4027
  promoteNextBgQueuedToRunning() {
3671
4028
  if (this.state.messages.some(function(m) {
3672
4029
  return m.isPending && m.role === "assistant" && m.isBackgroundTask;
@@ -3792,6 +4149,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3792
4149
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
3793
4150
  else this.state.messages.push(msg);
3794
4151
  }
4152
+ /**
4153
+ * The server's OWN copy of this turn is already on screen.
4154
+ *
4155
+ * A first-page fetch can land between the server settling the item and this
4156
+ * poll's tick, and now that the local bubbles carry the item id
4157
+ * (_stampTurnWithItemId) the rescue correctly drops them and renders the
4158
+ * server's settled pair instead. There is then nothing left to resolve: the
4159
+ * -1 fallback would push the answer in a SECOND time at the bottom of the list,
4160
+ * and the positional fallbacks would hijack some other turn's bubble.
4161
+ *
4162
+ * The USER bubble counts, not just a settled assistant: an item whose answer is
4163
+ * empty produces no assistant bubble at all in the mapper, and that variant
4164
+ * would otherwise still bottom-push "No text response received...". While a turn
4165
+ * is genuinely live its user bubble is always pending (the queued branch sets
4166
+ * isPendingQueued, promoteNextQueuedToRunning sets isPendingInProcess), so this
4167
+ * cannot fire early.
4168
+ */
4169
+ _turnAlreadyRendered(serverId) {
4170
+ if (!serverId) return false;
4171
+ return this.state.messages.some(function(m) {
4172
+ return m._serverItemId === serverId && !m.isPending && !m.isPendingQueued && !m.isPendingInProcess;
4173
+ });
4174
+ }
3795
4175
  onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
3796
4176
  if (serverId) this.historyItemPolls.delete(serverId);
3797
4177
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
@@ -3800,6 +4180,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3800
4180
  if (serverId) this.cancelledServerIds.delete(serverId);
3801
4181
  return;
3802
4182
  }
4183
+ if (this._turnAlreadyRendered(serverId)) {
4184
+ if (serverId) this.cancelledServerIds.delete(serverId);
4185
+ this.host.notify();
4186
+ this.updateHistoryCache();
4187
+ return;
4188
+ }
3803
4189
  var targetIdx = this.resolveQueuedUserBubble(serverId);
3804
4190
  if (targetIdx === void 0) {
3805
4191
  this.host.notify();
@@ -3841,6 +4227,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
3841
4227
  if (serverId) this.cancelledServerIds.delete(serverId);
3842
4228
  return;
3843
4229
  }
4230
+ if (this._turnAlreadyRendered(serverId)) {
4231
+ if (serverId) this.cancelledServerIds.delete(serverId);
4232
+ this.host.notify();
4233
+ this.updateHistoryCache();
4234
+ return;
4235
+ }
3844
4236
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
3845
4237
  if (isNotExists) {
3846
4238
  var userIdx = serverId ? this.state.messages.findIndex(function(m) {
@@ -5102,21 +5494,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5102
5494
  var rescued = [];
5103
5495
  for (var ri = 0; ri < self.state.messages.length; ri++) {
5104
5496
  var mm = self.state.messages[ri];
5105
- if (mm.isBackgroundTask) continue;
5106
- if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
5107
- if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
5108
- if (!mm._serverItemId) {
5109
- if (mm._stageId) {
5110
- rescued.push(mm);
5111
- continue;
5112
- }
5113
- if (mappedHasPendingAssistant) continue;
5114
- if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
5115
- else if (self.state.sending && mm.role === "user") {
5116
- var next = self.state.messages[ri + 1];
5117
- if (next && !next.isBackgroundTask && next.isPending && !next._serverItemId) rescued.push(mm);
5118
- }
5119
- }
5497
+ if (shouldRescueInFlightMessage(mm, {
5498
+ hasServerId: function(sid) {
5499
+ return !!serverIds[sid];
5500
+ },
5501
+ pageHasPendingAssistant: mappedHasPendingAssistant,
5502
+ sending: !!self.state.sending,
5503
+ next: self.state.messages[ri + 1],
5504
+ loadKey
5505
+ })) rescued.push(mm);
5120
5506
  }
5121
5507
  var oldestInPage1 = void 0;
5122
5508
  mapped.forEach(function(m) {
@@ -5167,6 +5553,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5167
5553
  keptOlderPages = prependOlder.length > 0 || interleave.length > 0;
5168
5554
  self.state.messages = prependOlder.length ? prependOlder.concat(page1) : page1;
5169
5555
  rescued.forEach(function(m) {
5556
+ if (m._serverItemId && self.state.messages.some(function(x) {
5557
+ return x._serverItemId === m._serverItemId && x.role === m.role;
5558
+ })) return;
5170
5559
  self.state.messages.push(m);
5171
5560
  });
5172
5561
  if (Object.keys(locallyCancelled).length) {
@@ -5318,8 +5707,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5318
5707
  if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
5319
5708
  if ((item._isBgTask || item._isOnBgQueue) && !bgAllow[item.id]) return;
5320
5709
  var capturedId = item.id;
5321
- var pp = item.poll({
5322
- latency: POLL_INTERVAL,
5710
+ var isBg = !!(item._isBgTask || item._isOnBgQueue);
5711
+ var pollOpts = {
5323
5712
  onResponse: function(response) {
5324
5713
  if (isPollStopped(response)) return;
5325
5714
  self.handleHistoryItemResolution(capturedId, response, platform);
@@ -5334,9 +5723,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5334
5723
  var uIdx = self.state.messages.findIndex(function(m) {
5335
5724
  return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
5336
5725
  });
5337
- var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
5726
+ var isBg2 = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
5338
5727
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
5339
- if (!isBg) {
5728
+ if (!isBg2) {
5340
5729
  if (uIdx !== -1) {
5341
5730
  var ex = self.state.messages[uIdx];
5342
5731
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
@@ -5363,7 +5752,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5363
5752
  self.updateHistoryCache();
5364
5753
  }
5365
5754
  }
5366
- });
5755
+ };
5756
+ var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts);
5367
5757
  self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
5368
5758
  if (pp && pp.catch) pp.catch(function() {
5369
5759
  });
@@ -5998,7 +6388,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
5998
6388
  (function() {
5999
6389
  var MCP_PROD = "https://mcp.broadwayinc.computer";
6000
6390
  var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
6001
- var BQ_VERSION = "1.8.12" ;
6391
+ var BQ_VERSION = "1.8.14" ;
6002
6392
  var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
6003
6393
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
6004
6394
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -7950,12 +8340,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
7950
8340
  else CS.messagesBox.scrollTop = CS.messagesBox.scrollHeight;
7951
8341
  });
7952
8342
  }
8343
+ var lastHistoryScrollTop = 0;
7953
8344
  function onHistoryScroll() {
7954
8345
  if (!CS.messagesBox || CS.chatSettingsOpen) return;
7955
8346
  var el = CS.messagesBox;
7956
- CS.stickToBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 16;
8347
+ var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 16;
8348
+ if (!atBottom) CS.stickToBottom = false;
8349
+ else if (el.scrollTop >= lastHistoryScrollTop) CS.stickToBottom = true;
8350
+ lastHistoryScrollTop = el.scrollTop;
8351
+ chatScrollAnchor.remember();
7957
8352
  if (el.scrollTop <= 60) pageOlderHistoryUntilTaller();
7958
8353
  }
8354
+ function onMessagesFontsSettled() {
8355
+ chatScrollAnchor.hold();
8356
+ }
8357
+ function onMessagesImageSettled(e) {
8358
+ var t = e && e.target;
8359
+ if (!t || t.tagName !== "IMG") return;
8360
+ chatScrollAnchor.absorb(previewLayoutBox(t));
8361
+ }
7959
8362
  var _touchStartY = 0;
7960
8363
  function onMessagesWheel(e) {
7961
8364
  if (e.deltaY < 0) CS.stickToBottom = false;
@@ -8288,12 +8691,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8288
8691
  var uid = S.user && S.user.user_id;
8289
8692
  if (uid) body.uid = uid;
8290
8693
  }
8291
- return S.skapi.util.request("get-signed-url", body, reqOpts).then(function(res) {
8694
+ function unwrap(res) {
8292
8695
  var u = typeof res === "string" ? res : res && res.url;
8293
8696
  if (!u) throw new Error("No temporary URL returned.");
8294
8697
  if (/^https?:\/\//i.test(u)) return u;
8295
8698
  return "https://db." + hostDomain() + "/" + u;
8296
- });
8699
+ }
8700
+ return S.skapi.util.request("get-signed-url", body, reqOpts).then(unwrap);
8297
8701
  }
8298
8702
  var ATTACH_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
8299
8703
  var FILE_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
@@ -8843,6 +9247,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8843
9247
  clearLinkUnavailable(linkUnavailableKeysForPath(path));
8844
9248
  scrollToBottomIfSticky(false);
8845
9249
  },
9250
+ // The resizes an <img> makes with no event of its own to announce
9251
+ // them: the src landing, and the src being dropped for a retry (which
9252
+ // collapses an already-painted picture to nothing). load and error are
9253
+ // covered by the listener on the message box, which also catches the
9254
+ // markdown images this module never sees.
9255
+ onLayoutChange: function(img) {
9256
+ chatScrollAnchor.absorb(img);
9257
+ },
8846
9258
  // The mint was refused, or the url it minted would not load. Either
8847
9259
  // way there is no url for this file, so the caption chip left behind
8848
9260
  // must not keep offering a click that opens a dead tab.
@@ -8863,16 +9275,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
8863
9275
  }
8864
9276
  var refreshedLinkExpiryTimer = null;
8865
9277
  function expireAllRefreshedLinks() {
8866
- for (var k in refreshedExpiredLinkMap) delete refreshedExpiredLinkMap[k];
8867
- for (var u in unavailableLinkMap) delete unavailableLinkMap[u];
9278
+ var changed = false;
9279
+ for (var k in refreshedExpiredLinkMap) {
9280
+ delete refreshedExpiredLinkMap[k];
9281
+ changed = true;
9282
+ }
9283
+ for (var u in unavailableLinkMap) {
9284
+ delete unavailableLinkMap[u];
9285
+ changed = true;
9286
+ }
9287
+ return changed;
8868
9288
  }
8869
9289
  function scheduleNextLinkExpiryBoundary() {
8870
9290
  if (refreshedLinkExpiryTimer) clearTimeout(refreshedLinkExpiryTimer);
8871
9291
  var now = Date.now();
8872
9292
  var next = Math.ceil(now / LINK_REFRESH_WINDOW_MS) * LINK_REFRESH_WINDOW_MS;
8873
9293
  refreshedLinkExpiryTimer = setTimeout(function() {
8874
- expireAllRefreshedLinks();
8875
- renderMessages();
9294
+ if (expireAllRefreshedLinks()) renderMessages();
8876
9295
  scheduleNextLinkExpiryBoundary();
8877
9296
  }, Math.max(1, next - now));
8878
9297
  }
@@ -9608,40 +10027,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9608
10027
  function indexGroupAnchorId(group) {
9609
10028
  return group.anchorId || "";
9610
10029
  }
10030
+ var chatScrollAnchor = createScrollAnchor({
10031
+ getBox: function() {
10032
+ return CS.messagesBox;
10033
+ },
10034
+ isStuck: function() {
10035
+ return !!CS.stickToBottom;
10036
+ },
10037
+ rawFallback: true
10038
+ });
9611
10039
  function captureScrollAnchor() {
9612
- var box = CS.messagesBox;
9613
- if (!box || CS.stickToBottom) return null;
9614
- var boxTop = box.getBoundingClientRect().top;
9615
- var kids = box.children;
9616
- var fallback = null;
9617
- for (var i = 0; i < kids.length; i++) {
9618
- if (!kids[i].getAttribute) continue;
9619
- var key = kids[i].getAttribute("data-row-key");
9620
- if (!key) continue;
9621
- var top = kids[i].getBoundingClientRect().top - boxTop;
9622
- if (top + kids[i].offsetHeight <= 0) continue;
9623
- var cand = { key, top, scrollTop: box.scrollTop, pos: kids[i].getAttribute("data-row-pos") };
9624
- if (cand.pos === null) return cand;
9625
- if (!fallback) fallback = cand;
9626
- }
9627
- return fallback || { key: null, top: 0, scrollTop: box.scrollTop };
10040
+ return chatScrollAnchor.capture();
9628
10041
  }
9629
10042
  function restoreScrollAnchor(anchor) {
9630
10043
  var box = CS.messagesBox;
9631
10044
  if (!box) return;
9632
- if (!anchor || CS.stickToBottom) {
10045
+ if (CS.stickToBottom) {
9633
10046
  box.scrollTop = box.scrollHeight;
9634
10047
  return;
9635
10048
  }
9636
- var boxTop = box.getBoundingClientRect().top;
9637
- var kids = box.children;
9638
- for (var i = 0; anchor.key && i < kids.length; i++) {
9639
- if (!kids[i].getAttribute || kids[i].getAttribute("data-row-key") !== anchor.key) continue;
9640
- if (anchor.pos && kids[i].getAttribute("data-row-pos") !== anchor.pos) break;
9641
- box.scrollTop += kids[i].getBoundingClientRect().top - boxTop - anchor.top;
9642
- return;
9643
- }
9644
- box.scrollTop = anchor.scrollTop;
10049
+ chatScrollAnchor.restore(anchor);
9645
10050
  }
9646
10051
  function syncDraftingIndicator() {
9647
10052
  if (!CS.messagesBox) return;
@@ -9684,6 +10089,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9684
10089
  if (CS.loadingHistory && !CS.loadingOlderHistory) {
9685
10090
  CS.messagesBox.appendChild(historyLoadingEl(true));
9686
10091
  syncDraftingIndicator();
10092
+ restoreScrollAnchor(anchor);
9687
10093
  return;
9688
10094
  }
9689
10095
  var emptyStubEls = [];
@@ -9692,12 +10098,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9692
10098
  for (var ge = 0; ge < emptyEntries.length; ge++) {
9693
10099
  if (emptyEntries[ge].kind !== "indexing") continue;
9694
10100
  var sg = emptyEntries[ge].group;
9695
- emptyStubEls.push(buildIndexGroupEl(sg, !!CS.indexGroupsOpen[sg.key]));
10101
+ var stubEl = buildIndexGroupEl(sg, !!CS.indexGroupsOpen[sg.key]);
10102
+ stubEl.setAttribute("data-row-key", "g" + sg.runKey);
10103
+ stubEl.setAttribute("data-row-pos", indexGroupAnchorId(sg));
10104
+ emptyStubEls.push(stubEl);
9696
10105
  }
9697
10106
  } catch (e) {
9698
10107
  }
9699
10108
  for (var gse = 0; gse < emptyStubEls.length; gse++) CS.messagesBox.appendChild(emptyStubEls[gse]);
9700
10109
  syncDraftingIndicator();
10110
+ restoreScrollAnchor(anchor);
9701
10111
  return;
9702
10112
  }
9703
10113
  var rows = buildChatDisplayList(CS.messages, displayListOptions());
@@ -9762,14 +10172,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9762
10172
  if (idx < 0 || idx >= CS.messages.length) return;
9763
10173
  var oldEl = CS.messageEls[idx];
9764
10174
  if (!oldEl || !oldEl.parentNode) return;
9765
- var newEl = buildMessageEl(CS.messages[idx], idx);
9766
- if (oldEl.classList.contains("bq-index-pass")) newEl.classList.add("bq-index-pass");
9767
- oldEl.parentNode.replaceChild(newEl, oldEl);
9768
- CS.messageEls[idx] = newEl;
10175
+ chatScrollAnchor.preserve(function() {
10176
+ var newEl = buildMessageEl(CS.messages[idx], idx);
10177
+ if (oldEl.classList.contains("bq-index-pass")) newEl.classList.add("bq-index-pass");
10178
+ oldEl.parentNode.replaceChild(newEl, oldEl);
10179
+ CS.messageEls[idx] = newEl;
10180
+ });
9769
10181
  hydrateMessageImagePreviews();
9770
10182
  }
9771
10183
  function renderChat() {
9772
10184
  clearImagePreviewCache(S.projectId || "default");
10185
+ chatScrollAnchor.forget();
9773
10186
  for (var uk in unavailableLinkMap) delete unavailableLinkMap[uk];
9774
10187
  CS.messages = [];
9775
10188
  CS.messageEls = [];
@@ -9844,6 +10257,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
9844
10257
  box.addEventListener("wheel", onMessagesWheel, { passive: true });
9845
10258
  box.addEventListener("touchstart", onMessagesTouchStart, { passive: true });
9846
10259
  box.addEventListener("touchmove", onMessagesTouchMove, { passive: true });
10260
+ box.addEventListener("load", onMessagesImageSettled, true);
10261
+ box.addEventListener("error", onMessagesImageSettled, true);
10262
+ if (document.fonts && document.fonts.addEventListener) {
10263
+ document.fonts.addEventListener("loadingdone", onMessagesFontsSettled);
10264
+ }
9847
10265
  CS.messagesBox = box;
9848
10266
  var input = h("textarea", { class: "bq-input", rows: "1", placeholder: "Ask anything about: " + (S.serviceName || "your project") });
9849
10267
  CS.inputEl = input;