bunnyquery 1.8.13 → 1.8.15

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
@@ -901,18 +901,31 @@ function classifyInlineLink(full, groups, ctx) {
901
901
  part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
902
902
  });
903
903
  }
904
+ function canonicalizePathForm(value) {
905
+ if (!value) return value;
906
+ try {
907
+ return value.normalize("NFC");
908
+ } catch (e) {
909
+ return value;
910
+ }
911
+ }
904
912
  function linkUnavailableKeyForPath(remotePath) {
905
- return "path:" + (remotePath || "");
913
+ return "path:" + canonicalizePathForm(remotePath || "");
906
914
  }
907
915
  function linkUnavailableKeyForHref(href) {
908
- return "href:" + (href || "");
916
+ var carried = readExpiredAttachmentHref(href);
917
+ if (carried) return linkUnavailableKeyForPath(carried);
918
+ return "href:" + canonicalizePathForm(href || "");
909
919
  }
910
920
  function linkUnavailableKeysForPath(remotePath) {
911
921
  if (!remotePath) return [];
912
- return [
922
+ var keys = [
913
923
  linkUnavailableKeyForPath(remotePath),
914
924
  linkUnavailableKeyForHref(buildDisplayExpiredAttachmentHref(remotePath))
915
925
  ];
926
+ return keys.filter(function(k, i) {
927
+ return keys.indexOf(k) === i;
928
+ });
916
929
  }
917
930
  function isLinkUnavailable(link, map) {
918
931
  if (!link || !map) return false;
@@ -1313,7 +1326,7 @@ function renderInlineLinkHtml(link, opts) {
1313
1326
  if (link.remotePath) attrs.push('data-bq-remote-path="' + escapeInlineHtml(link.remotePath) + '"');
1314
1327
  if (link.fullLabel) attrs.push('data-bq-full-label="' + escapeInlineHtml(link.fullLabel) + '"');
1315
1328
  if (!preview) return "<a " + attrs.join(" ") + ">" + escapeInlineHtml(labelText) + "</a>";
1316
- 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>";
1329
+ 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>";
1317
1330
  }
1318
1331
 
1319
1332
  // src/engine/image_preview.ts
@@ -1399,31 +1412,48 @@ function hydrateOne(img, ctx) {
1399
1412
  var warm = peekImagePreviewUrl(ctx, path);
1400
1413
  if (warm) {
1401
1414
  img.setAttribute("src", warm);
1415
+ notifyLayoutChange(img, ctx, path);
1402
1416
  return;
1403
1417
  }
1404
1418
  resolveImagePreviewUrl(ctx, path, type).then(function(url) {
1405
1419
  if (img.getAttribute("data-bq-img-state") !== "loading") return;
1406
1420
  img.setAttribute("src", url);
1421
+ notifyLayoutChange(img, ctx, path);
1407
1422
  }, function(e) {
1408
1423
  img.setAttribute("data-bq-img-state", "error");
1424
+ notifyLayoutChange(img, ctx, path);
1409
1425
  if (ctx.onError) ctx.onError(path, e);
1410
1426
  });
1411
1427
  }
1412
1428
  function onImageError(img, ctx, path, type) {
1413
1429
  if (img.getAttribute("data-bq-img-retry") === "1") {
1414
1430
  img.setAttribute("data-bq-img-state", "error");
1431
+ notifyLayoutChange(img, ctx, path);
1415
1432
  if (ctx.onError) ctx.onError(path, new Error("image preview failed to load"));
1416
1433
  return;
1417
1434
  }
1418
1435
  img.setAttribute("data-bq-img-retry", "1");
1419
1436
  img.removeAttribute("src");
1437
+ notifyLayoutChange(img, ctx, path);
1420
1438
  resolveImagePreviewUrl(ctx, path, type, true).then(function(url) {
1421
1439
  img.setAttribute("src", url);
1440
+ notifyLayoutChange(img, ctx, path);
1422
1441
  }, function(e) {
1423
1442
  img.setAttribute("data-bq-img-state", "error");
1443
+ notifyLayoutChange(img, ctx, path);
1424
1444
  if (ctx.onError) ctx.onError(path, e);
1425
1445
  });
1426
1446
  }
1447
+ function notifyLayoutChange(img, ctx, path) {
1448
+ if (!ctx.onLayoutChange) return;
1449
+ ctx.onLayoutChange(previewLayoutBox(img), path);
1450
+ }
1451
+ var PREVIEW_LAYOUT_BOX_SELECTOR = "a.bq-link-button.is-image-preview";
1452
+ function previewLayoutBox(img) {
1453
+ var closest = img.closest;
1454
+ if (typeof closest !== "function") return img;
1455
+ return closest.call(img, PREVIEW_LAYOUT_BOX_SELECTOR) || img;
1456
+ }
1427
1457
 
1428
1458
  // src/engine/time.ts
1429
1459
  function wallClockNow() {
@@ -2531,6 +2561,327 @@ function mapHistoryListToMessages(list, platform, opts) {
2531
2561
  }
2532
2562
  return { messages: mapped, runningItemIds };
2533
2563
  }
2564
+ function shouldRescueInFlightMessage(m, ctx) {
2565
+ if (!m) return false;
2566
+ if (m.isBackgroundTask) return false;
2567
+ if (m._ownerKey !== void 0 && ctx.loadKey !== void 0 && m._ownerKey !== ctx.loadKey) return false;
2568
+ if (m._serverItemId && ctx.hasServerId(m._serverItemId)) return false;
2569
+ if (m._stageId) return true;
2570
+ if (!m._serverItemId && ctx.pageHasPendingAssistant) return false;
2571
+ if (m.isSendingToServer || m.isPendingQueued || m.isPendingInProcess || m.isPending) return true;
2572
+ if (ctx.sending && m.role === "user") {
2573
+ var next = ctx.next;
2574
+ if (!next || next.isBackgroundTask || !next.isPending) return false;
2575
+ return next._serverItemId === void 0 || next._serverItemId === m._serverItemId;
2576
+ }
2577
+ return false;
2578
+ }
2579
+
2580
+ // src/engine/scroll_anchor.ts
2581
+ var ROW_KEY_ATTR = "data-row-key";
2582
+ var ROW_POS_ATTR = "data-row-pos";
2583
+ var MAX_ALTS = 2;
2584
+ var ALT_SCAN_LIMIT = 64;
2585
+ var UNKNOWN_ROW_POS = "\0?";
2586
+ function createScrollAnchor(options) {
2587
+ var held = null;
2588
+ var seen = typeof WeakMap === "function" ? /* @__PURE__ */ new WeakMap() : null;
2589
+ function capture() {
2590
+ var box = options.getBox();
2591
+ if (!box || options.isStuck()) return null;
2592
+ var boxTop = box.getBoundingClientRect().top;
2593
+ var kids = box.children;
2594
+ var fallback = null;
2595
+ var fallbackAt = -1;
2596
+ for (var i = 0; i < kids.length; i++) {
2597
+ var el = kids[i];
2598
+ if (!el || typeof el.getAttribute !== "function") continue;
2599
+ var key = el.getAttribute(ROW_KEY_ATTR);
2600
+ if (!key) continue;
2601
+ var top = el.getBoundingClientRect().top - boxTop;
2602
+ if (top + el.offsetHeight <= 0) continue;
2603
+ if (top >= box.clientHeight) break;
2604
+ var rawPos = el.getAttribute(ROW_POS_ATTR);
2605
+ var pos = rawPos === null ? null : rawPos || UNKNOWN_ROW_POS;
2606
+ var cand = {
2607
+ key,
2608
+ top,
2609
+ pos,
2610
+ scrollTop: box.scrollTop,
2611
+ scrollHeight: box.scrollHeight,
2612
+ el
2613
+ };
2614
+ if (rawPos === null) {
2615
+ cand.alts = collectAlts(box, boxTop, i + 1);
2616
+ return cand;
2617
+ }
2618
+ if (!fallback) {
2619
+ fallback = cand;
2620
+ fallbackAt = i;
2621
+ }
2622
+ }
2623
+ if (fallback) {
2624
+ fallback.alts = collectAlts(box, boxTop, fallbackAt + 1, true);
2625
+ return fallback;
2626
+ }
2627
+ return {
2628
+ key: null,
2629
+ top: 0,
2630
+ pos: null,
2631
+ scrollTop: box.scrollTop,
2632
+ scrollHeight: box.scrollHeight,
2633
+ el: null
2634
+ };
2635
+ }
2636
+ function collectAlts(box, boxTop, from, ordinaryOnly) {
2637
+ var out = [];
2638
+ var kids = box.children;
2639
+ var stop = Math.min(kids.length, from + ALT_SCAN_LIMIT);
2640
+ for (var i = from; i < stop && out.length < MAX_ALTS; i++) {
2641
+ var el = kids[i];
2642
+ if (!el || typeof el.getAttribute !== "function") continue;
2643
+ var key = el.getAttribute(ROW_KEY_ATTR);
2644
+ if (!key) continue;
2645
+ var rawPos = el.getAttribute(ROW_POS_ATTR);
2646
+ if (ordinaryOnly && rawPos !== null) continue;
2647
+ out.push({
2648
+ key,
2649
+ top: el.getBoundingClientRect().top - boxTop,
2650
+ pos: rawPos === null ? null : rawPos || UNKNOWN_ROW_POS,
2651
+ el
2652
+ });
2653
+ }
2654
+ return out.length ? out : void 0;
2655
+ }
2656
+ function findRow(box, anchor) {
2657
+ var el = anchor.el;
2658
+ if (el && el.parentNode === box) return el;
2659
+ if (!anchor.key) return null;
2660
+ var kids = box.children;
2661
+ for (var i = 0; i < kids.length; i++) {
2662
+ var kid = kids[i];
2663
+ if (!kid || typeof kid.getAttribute !== "function") continue;
2664
+ if (kid.getAttribute(ROW_KEY_ATTR) === anchor.key) return kid;
2665
+ }
2666
+ return null;
2667
+ }
2668
+ var sawFrozen = false;
2669
+ var parked = null;
2670
+ var parkedStuck = false;
2671
+ var returning = false;
2672
+ var wroteTop = -1;
2673
+ var restoreExact = true;
2674
+ var restorePinned = false;
2675
+ function frozen() {
2676
+ var f = !!options.isFrozen && options.isFrozen();
2677
+ if (f) sawFrozen = true;
2678
+ return f;
2679
+ }
2680
+ function restore(anchor, unbounded) {
2681
+ if (frozen()) return;
2682
+ var box = options.getBox();
2683
+ if (!box || !anchor || options.isStuck()) return;
2684
+ var el = findRow(box, anchor);
2685
+ if (el) {
2686
+ var livePos = el.getAttribute(ROW_POS_ATTR) || UNKNOWN_ROW_POS;
2687
+ if (anchor.pos !== null && anchor.pos !== UNKNOWN_ROW_POS && livePos !== UNKNOWN_ROW_POS && livePos !== anchor.pos) el = null;
2688
+ }
2689
+ if (el) {
2690
+ var boxTop = box.getBoundingClientRect().top;
2691
+ var delta = el.getBoundingClientRect().top - boxTop - anchor.top;
2692
+ var slack = Math.abs(box.scrollHeight - anchor.scrollHeight) + box.clientHeight;
2693
+ if (!unbounded && (delta > slack || delta < -slack)) {
2694
+ lost(box, anchor);
2695
+ return;
2696
+ }
2697
+ var want = box.scrollTop + delta;
2698
+ if (delta >= 1 || delta <= -1) box.scrollTop += delta;
2699
+ wroteTop = box.scrollTop;
2700
+ restorePinned = true;
2701
+ restoreExact = box.scrollTop >= want - 1 && box.scrollTop <= want + 1;
2702
+ held = {
2703
+ key: anchor.key,
2704
+ top: anchor.top,
2705
+ pos: anchor.pos,
2706
+ scrollTop: box.scrollTop,
2707
+ scrollHeight: box.scrollHeight,
2708
+ el,
2709
+ // Carried, not dropped: one successful restore used to disarm the
2710
+ // standbys for every later hold.
2711
+ alts: anchor.alts
2712
+ };
2713
+ return;
2714
+ }
2715
+ var alts = anchor.alts;
2716
+ for (var ai = 0; alts && ai < alts.length; ai++) {
2717
+ var alt = alts[ai];
2718
+ var ael = findRow(box, { key: alt.key, top: alt.top, pos: alt.pos, scrollTop: anchor.scrollTop, scrollHeight: anchor.scrollHeight, el: alt.el });
2719
+ if (!ael) continue;
2720
+ if (alt.pos !== null && alt.pos !== UNKNOWN_ROW_POS) {
2721
+ var altLive = ael.getAttribute(ROW_POS_ATTR) || UNKNOWN_ROW_POS;
2722
+ if (altLive !== UNKNOWN_ROW_POS && altLive !== alt.pos) continue;
2723
+ }
2724
+ var aboxTop = box.getBoundingClientRect().top;
2725
+ var adelta = ael.getBoundingClientRect().top - aboxTop - alt.top;
2726
+ var aslack = Math.abs(box.scrollHeight - anchor.scrollHeight) + box.clientHeight;
2727
+ if (!unbounded && (adelta > aslack || adelta < -aslack)) continue;
2728
+ var awant = box.scrollTop + adelta;
2729
+ if (adelta >= 1 || adelta <= -1) box.scrollTop += adelta;
2730
+ wroteTop = box.scrollTop;
2731
+ restorePinned = true;
2732
+ restoreExact = box.scrollTop >= awant - 1 && box.scrollTop <= awant + 1;
2733
+ held = {
2734
+ key: alt.key,
2735
+ top: alt.top,
2736
+ pos: alt.pos,
2737
+ scrollTop: box.scrollTop,
2738
+ scrollHeight: box.scrollHeight,
2739
+ el: ael,
2740
+ alts: alts.slice(ai + 1)
2741
+ };
2742
+ return;
2743
+ }
2744
+ lost(box, anchor);
2745
+ }
2746
+ function lost(box, anchor) {
2747
+ held = null;
2748
+ var grew = box.scrollHeight - anchor.scrollHeight;
2749
+ if (grew > 0) {
2750
+ box.scrollTop = anchor.scrollTop + grew;
2751
+ wroteTop = box.scrollTop;
2752
+ return;
2753
+ }
2754
+ if (options.rawFallback) {
2755
+ box.scrollTop = anchor.scrollTop;
2756
+ wroteTop = box.scrollTop;
2757
+ }
2758
+ }
2759
+ function preserve(mutate) {
2760
+ var anchor = capture();
2761
+ var result = mutate();
2762
+ restore(anchor);
2763
+ return result;
2764
+ }
2765
+ function remember() {
2766
+ var b0 = options.getBox();
2767
+ if (returning && b0 && b0.scrollTop !== wroteTop) {
2768
+ parked = null;
2769
+ parkedStuck = false;
2770
+ returning = false;
2771
+ }
2772
+ if (frozen()) return;
2773
+ held = capture();
2774
+ }
2775
+ function hold() {
2776
+ if (frozen()) return;
2777
+ if (sawFrozen || returning) {
2778
+ settleReturn();
2779
+ return;
2780
+ }
2781
+ var box = options.getBox();
2782
+ if (!box || options.isStuck()) {
2783
+ held = null;
2784
+ return;
2785
+ }
2786
+ if (!held) {
2787
+ held = capture();
2788
+ return;
2789
+ }
2790
+ if (box.scrollTop !== held.scrollTop) {
2791
+ held = capture();
2792
+ return;
2793
+ }
2794
+ restore(held);
2795
+ }
2796
+ function absorb(el) {
2797
+ if (!el) return;
2798
+ var box = options.getBox();
2799
+ if (!box) return;
2800
+ var h = el.offsetHeight;
2801
+ var prev = seen ? seen.get(el) : void 0;
2802
+ if (seen) seen.set(el, h);
2803
+ if (options.isStuck()) return;
2804
+ if (prev === void 0) prev = 0;
2805
+ var delta = h - prev;
2806
+ if (delta === 0) return;
2807
+ if (frozen()) {
2808
+ foldFrozenGrowth(box, el, delta);
2809
+ return;
2810
+ }
2811
+ if (el.getBoundingClientRect().top >= box.getBoundingClientRect().top) return;
2812
+ box.scrollTop += delta;
2813
+ wroteTop = box.scrollTop;
2814
+ if (held) held = capture();
2815
+ }
2816
+ function park() {
2817
+ parked = capture();
2818
+ parkedStuck = !!options.isStuck();
2819
+ returning = true;
2820
+ }
2821
+ function pinBottom() {
2822
+ var box = options.getBox();
2823
+ if (!box) return;
2824
+ box.scrollTop = box.scrollHeight;
2825
+ wroteTop = box.scrollTop;
2826
+ }
2827
+ function settleReturn() {
2828
+ if (frozen()) return false;
2829
+ sawFrozen = false;
2830
+ if (parkedStuck) {
2831
+ pinBottom();
2832
+ return true;
2833
+ }
2834
+ var target = parked || held;
2835
+ restoreExact = true;
2836
+ restorePinned = false;
2837
+ restore(target, true);
2838
+ parked = !restorePinned || !restoreExact ? target : null;
2839
+ returning = !!parked;
2840
+ return false;
2841
+ }
2842
+ function isReturning() {
2843
+ return returning;
2844
+ }
2845
+ function thaw() {
2846
+ settleReturn();
2847
+ }
2848
+ function foldFrozenGrowth(box, el, delta) {
2849
+ var elTop = el.getBoundingClientRect().top;
2850
+ foldInto(box, held, elTop, delta);
2851
+ foldInto(box, parked, elTop, delta);
2852
+ }
2853
+ function foldInto(box, a, elTop, delta) {
2854
+ if (!a || a.top >= 0) return;
2855
+ var rowEl = findRow(box, a);
2856
+ if (!rowEl) return;
2857
+ var within = elTop - rowEl.getBoundingClientRect().top;
2858
+ if (within < 0 || within >= rowEl.offsetHeight) return;
2859
+ if (within >= -a.top) return;
2860
+ a.top -= delta;
2861
+ a.scrollHeight += delta;
2862
+ }
2863
+ function forget() {
2864
+ held = null;
2865
+ parked = null;
2866
+ parkedStuck = false;
2867
+ returning = false;
2868
+ }
2869
+ return {
2870
+ capture,
2871
+ restore,
2872
+ preserve,
2873
+ remember,
2874
+ hold,
2875
+ park,
2876
+ settleReturn,
2877
+ isReturning,
2878
+ pinBottom,
2879
+ isFrozen: frozen,
2880
+ thaw,
2881
+ absorb,
2882
+ forget
2883
+ };
2884
+ }
2534
2885
 
2535
2886
  // src/engine/viewport_fill.ts
2536
2887
  var HISTORY_FILL_SLACK_PX = 64;
@@ -2642,6 +2993,7 @@ var WORKER_PASS_ADOPT_LIMIT = 20;
2642
2993
  var LIVE_INDEX_SNAPSHOT_MAX_AGE_MS = 5e3;
2643
2994
  var INDEX_DISPATCH_CLAIM_MS = 2 * 60 * 1e3;
2644
2995
  var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
2996
+ var EARLY_PROBE_SCHEDULE_MS = [400, 900, 1700];
2645
2997
  var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
2646
2998
  var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
2647
2999
  var INDEXING_DRAIN_IDLE_LOOKS = 2;
@@ -3037,7 +3389,93 @@ var ChatSession = class {
3037
3389
  *
3038
3390
  * `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
3039
3391
  * poll simply cannot be stopped and is left running — see pausePolling.
3392
+ *
3393
+ * (This block documents _trackPoll, further down. The two methods below sit between it
3394
+ * and its subject.)
3040
3395
  */
3396
+ /**
3397
+ * Foreground poll with an early-probe race.
3398
+ *
3399
+ * skapi's poll() is a bare setInterval(fn, latency) with NO check at t=0, so the earliest a
3400
+ * reply can be observed is one full POLL_INTERVAL (3s) after dispatch. For a long generation
3401
+ * that granularity is free. For a SHORT one it is nearly pure dead time: a greeting that the
3402
+ * provider finishes in 1s still waits until the 3s tick, which measured as a large share of a
3403
+ * 5s "yo" round trip.
3404
+ *
3405
+ * So keep the 3s interval as the steady state, and additionally point-look-up the item a few
3406
+ * times early, on a widening schedule. Whichever answers first wins and the other is stopped.
3407
+ * The probe uses the csrHistoryItemLookup hook both clients already implement; without it this
3408
+ * degrades to exactly the old behaviour.
3409
+ *
3410
+ * FOREGROUND ONLY. Background indexing polls keep the flat cadence: nobody is watching them,
3411
+ * and they are the ones bounded by MAX_CONCURRENT_BG_POLLS, so adding probes there would spend
3412
+ * the request budget the cap exists to protect.
3413
+ */
3414
+ attachForegroundPoll(source, itemId, opts) {
3415
+ return this._fgPollWithEarlyProbe(source, itemId, opts);
3416
+ }
3417
+ _fgPollWithEarlyProbe(source, itemId, opts) {
3418
+ var base = source.poll(Object.assign({ latency: POLL_INTERVAL }, opts || {}));
3419
+ var lookup = chatEngineConfig().csrHistoryItemLookup;
3420
+ var ident = this.host.getIdentity();
3421
+ var platform = ident && ident.platform;
3422
+ if (!lookup || !itemId || !ident || !ident.projectId || platform !== "claude" && platform !== "openai") {
3423
+ return base;
3424
+ }
3425
+ var settled = false;
3426
+ var timers = [];
3427
+ var clearProbes = function() {
3428
+ for (var i = 0; i < timers.length; i++) clearTimeout(timers[i]);
3429
+ timers = [];
3430
+ };
3431
+ var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
3432
+ var raced = new Promise(function(resolve, reject) {
3433
+ base.then(function(res) {
3434
+ if (settled) return;
3435
+ settled = true;
3436
+ clearProbes();
3437
+ resolve(res);
3438
+ }, function(err) {
3439
+ if (settled) return;
3440
+ settled = true;
3441
+ clearProbes();
3442
+ reject(err);
3443
+ });
3444
+ var fullId = buildHistoryItemFullId(platform, ident.projectId, itemId);
3445
+ EARLY_PROBE_SCHEDULE_MS.forEach(function(delay) {
3446
+ timers.push(setTimeout(function() {
3447
+ if (settled) return;
3448
+ Promise.resolve(lookup(fullId, ident.projectId, ident.owner)).then(function(body) {
3449
+ if (settled || !body || typeof body !== "object") return;
3450
+ if (body.status === "pending" || body.status === "running") return;
3451
+ if (isPollStopped(body)) return;
3452
+ settled = true;
3453
+ clearProbes();
3454
+ if (stopBase) {
3455
+ try {
3456
+ stopBase();
3457
+ } catch (e) {
3458
+ }
3459
+ }
3460
+ if (opts && typeof opts.onResponse === "function") {
3461
+ try {
3462
+ opts.onResponse(body);
3463
+ } catch (e) {
3464
+ }
3465
+ }
3466
+ resolve(body);
3467
+ }, function() {
3468
+ });
3469
+ }, delay));
3470
+ });
3471
+ });
3472
+ raced.stop = function() {
3473
+ settled = true;
3474
+ clearProbes();
3475
+ if (stopBase) stopBase();
3476
+ };
3477
+ return raced;
3478
+ }
3041
3479
  _trackPoll(id, kind, p) {
3042
3480
  var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
3043
3481
  if (!stop) {
@@ -3213,6 +3651,68 @@ var ChatSession = class {
3213
3651
  startKeyHistory: this.state.historyStartKeyHistory.slice()
3214
3652
  };
3215
3653
  }
3654
+ /**
3655
+ * Give the immediate-send pair the server's id for their turn, the moment the
3656
+ * dispatch learns it.
3657
+ *
3658
+ * WHY THIS EXISTS. An immediate send pushes its user bubble and its
3659
+ * "Thinking..." placeholder locally, and until now neither ever carried a
3660
+ * _serverItemId — only the QUEUED path stamped one, off its ack. So for the
3661
+ * whole life of the turn there was no way to tell the local copy and the
3662
+ * server's copy of the SAME turn apart, and the history merge fell back to a
3663
+ * heuristic: rescue the local pair unless the freshly-fetched page happens to
3664
+ * contain a pending assistant.
3665
+ *
3666
+ * That heuristic has a hole exactly one poll interval wide. The server settles
3667
+ * the request; for up to POLL_INTERVAL the client has not noticed, so
3668
+ * `state.sending` is still true and the local pair is still on screen — while a
3669
+ * history fetch issued in that window returns the turn ALREADY SETTLED, with no
3670
+ * pending assistant in it. The rescue then re-appends the local pair below the
3671
+ * server's copy (the question, twice), and when the poll finally resolves,
3672
+ * typewriteLatestReply writes the answer into the rescued placeholder because it
3673
+ * is the only pending assistant left (the answer, twice). updateHistoryCache
3674
+ * persists the result, so it survives every later visit.
3675
+ *
3676
+ * Navigating away while waiting and coming back is what lands a fetch in that
3677
+ * window: a remount runs refreshGate -> a fresh first page, at an arbitrary
3678
+ * moment relative to the 3s poll.
3679
+ *
3680
+ * With the id on the bubbles, both clients' rescue loops skip them through the
3681
+ * dedup they already have (`_serverItemId is in this page`), the reply the
3682
+ * dispatch caches inherits the id too, and nothing needs a new special case.
3683
+ *
3684
+ * Matched by _localId, never by index: a file's indexing rows are spliced in
3685
+ * above these bubbles while the request is in flight.
3686
+ */
3687
+ _stampTurnWithItemId(key, userLid, placeholderLid, itemId) {
3688
+ if (!itemId) return;
3689
+ var changed = false;
3690
+ var stamp = function(list) {
3691
+ var hit = false;
3692
+ for (var i = 0; i < list.length; i++) {
3693
+ var m = list[i];
3694
+ if (!m || !m._localId) continue;
3695
+ if (m._localId !== userLid && m._localId !== placeholderLid) continue;
3696
+ if (m._serverItemId === itemId) continue;
3697
+ list[i] = Object.assign({}, m, { _serverItemId: itemId });
3698
+ hit = true;
3699
+ }
3700
+ return hit;
3701
+ };
3702
+ changed = stamp(this.state.messages);
3703
+ var cached = key ? this.aiChatHistoryCache[key] : void 0;
3704
+ if (cached) {
3705
+ var msgs = cached.messages.slice();
3706
+ if (stamp(msgs)) {
3707
+ this.aiChatHistoryCache[key] = {
3708
+ messages: msgs,
3709
+ endOfList: cached.endOfList,
3710
+ startKeyHistory: cached.startKeyHistory
3711
+ };
3712
+ }
3713
+ }
3714
+ if (changed) this.host.notify();
3715
+ }
3216
3716
  /**
3217
3717
  * Land a resolved reply in the history cache of a chat that is NOT currently
3218
3718
  * visible, without touching state.messages. Mirrors the cache-only path in
@@ -3294,8 +3794,9 @@ var ChatSession = class {
3294
3794
  if (initial.id) {
3295
3795
  if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
3296
3796
  dispatchItemId = initial.id;
3797
+ if (typeof params.onItemId === "function") params.onItemId(initial.id);
3297
3798
  }
3298
- var dp = initial.poll({ latency: POLL_INTERVAL });
3799
+ var dp = self._fgPollWithEarlyProbe(initial, initial.id);
3299
3800
  if (initial.id) self._trackPoll(initial.id, "fg", dp);
3300
3801
  return dp;
3301
3802
  }
@@ -3320,28 +3821,9 @@ var ChatSession = class {
3320
3821
  }).then(function(result) {
3321
3822
  delete self.pendingAgentRequests[params.key];
3322
3823
  if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
3323
- var existing = self.aiChatHistoryCache[params.key] || { messages: [], endOfList: false, startKeyHistory: [] };
3324
3824
  var reply = { role: "assistant", content: result.content, isError: result.isError };
3325
- var msgs = existing.messages.slice();
3326
- var idx = -1;
3327
- for (var i = msgs.length - 1; i >= 0; i--) {
3328
- var m = msgs[i];
3329
- if (m && m.isPending && m.role === "assistant" && !m.isBackgroundTask) {
3330
- idx = i;
3331
- break;
3332
- }
3333
- }
3334
- if (idx !== -1) {
3335
- reply._serverItemId = msgs[idx]._serverItemId;
3336
- msgs[idx] = reply;
3337
- } else {
3338
- msgs.push(reply);
3339
- }
3340
- self.aiChatHistoryCache[params.key] = {
3341
- messages: msgs,
3342
- endOfList: existing.endOfList,
3343
- startKeyHistory: existing.startKeyHistory
3344
- };
3825
+ if (dispatchItemId) reply._serverItemId = dispatchItemId;
3826
+ self._applyReplyToCache(params.key, reply, dispatchItemId);
3345
3827
  return result;
3346
3828
  });
3347
3829
  this.pendingAgentRequests[params.key] = run;
@@ -3730,8 +4212,9 @@ var ChatSession = class {
3730
4212
  }
3731
4213
  this.host.notify();
3732
4214
  this.updateHistoryCache();
3733
- this.host.scrollToBottom(true);
4215
+ this.scrollForDispatch(stageId);
3734
4216
  var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
4217
+ var capturedQueuedLid = queuedBubble._localId;
3735
4218
  Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.projectId, id.owner)).then(function(result) {
3736
4219
  var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
3737
4220
  return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
@@ -3743,8 +4226,9 @@ var ChatSession = class {
3743
4226
  self.state.messages[sendingIdx] = upd;
3744
4227
  self.host.notify();
3745
4228
  }
4229
+ if (serverId) self._stampTurnWithItemId(capturedKey, capturedQueuedLid, void 0, serverId);
3746
4230
  if (result && result.poll && (result.status === "pending" || result.status === "running")) {
3747
- var qp = result.poll({ latency: POLL_INTERVAL });
4231
+ var qp = self._fgPollWithEarlyProbe(result, serverId);
3748
4232
  if (serverId) self._trackPoll(serverId, "fg", qp);
3749
4233
  return qp.then(function(res) {
3750
4234
  if (isPollStopped(res)) return;
@@ -3760,7 +4244,7 @@ var ChatSession = class {
3760
4244
  return;
3761
4245
  }
3762
4246
  var immediateUser = { role: "user", content: composed, _localId: this._newLocalId(), _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
3763
- var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
4247
+ var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _localId: this._newLocalId(), ...key ? { _ownerKey: key } : {} };
3764
4248
  var iStage = this._stageIndex(this.state.messages, stageId);
3765
4249
  if (iStage !== -1) {
3766
4250
  var iEx = this.state.messages[iStage];
@@ -3774,7 +4258,7 @@ var ChatSession = class {
3774
4258
  this.host.notify();
3775
4259
  this.updateHistoryCache();
3776
4260
  this.state.sending = true;
3777
- this.host.scrollToBottom(true);
4261
+ this.scrollForDispatch(stageId);
3778
4262
  var historyForLlm = this.state.messages.filter(function(m) {
3779
4263
  if (m === immediateUser) return false;
3780
4264
  return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask && !m.isError;
@@ -3787,6 +4271,7 @@ var ChatSession = class {
3787
4271
  projectId: id.projectId,
3788
4272
  history: historyForLlm
3789
4273
  });
4274
+ var immediateUserLid = immediateUser._localId, immediatePlaceholderLid = immediatePlaceholder._localId;
3790
4275
  var run = this.dispatchAgentRequest({
3791
4276
  key,
3792
4277
  projectId: id.projectId,
@@ -3798,7 +4283,15 @@ var ChatSession = class {
3798
4283
  boundedMessages: bounded.messages,
3799
4284
  userId: chatQueue,
3800
4285
  extractContent,
3801
- fileUrls
4286
+ fileUrls,
4287
+ // THE fix for a turn rendering twice after navigate-away-and-back. Until
4288
+ // this existed the immediate-send pair carried no server id for its whole
4289
+ // life (only the QUEUED path stamped one, off its ack), so nothing could
4290
+ // tell the local copy and the server's copy of the same turn apart. See
4291
+ // _stampTurnWithItemId.
4292
+ onItemId: function(itemId) {
4293
+ self._stampTurnWithItemId(key, immediateUserLid, immediatePlaceholderLid, itemId);
4294
+ }
3802
4295
  });
3803
4296
  Promise.resolve(run).catch(function() {
3804
4297
  }).then(function() {
@@ -3809,6 +4302,28 @@ var ChatSession = class {
3809
4302
  });
3810
4303
  });
3811
4304
  }
4305
+ /**
4306
+ * Scroll for a dispatch that is going out NOW, but never for one arriving late.
4307
+ *
4308
+ * A turn with attachments does not dispatch when the user hits Send: it waits out
4309
+ * its uploads and then its whole indexing chain, which is minutes
4310
+ * (awaitIndexingDrained). By then the reader has very often scrolled up into
4311
+ * history to pass the time, and the forcing scrollToBottom yanked them out of it
4312
+ * — and worse, force-pinned stickToBottom, which no-ops every method on the
4313
+ * scroll anchor and re-arms the queue-detect poll whose only bail is
4314
+ * !stickToBottom.
4315
+ *
4316
+ * `stageId` is the exact marker for that case: only the attachment path ever
4317
+ * produces one. The gesture itself was already paid for at stage time, where
4318
+ * stageOutgoingMessage forces the scroll while the user is still looking at the
4319
+ * composer. Deliberately NOT gated on "did we find the staged bubble" — a remount
4320
+ * rebuilds from the cache and the turn is appended rather than replaced, which is
4321
+ * just as late and just as unrequested.
4322
+ */
4323
+ scrollForDispatch(stageId) {
4324
+ if (stageId) this.host.scrollToBottomIfSticky(true);
4325
+ else this.host.scrollToBottom(true);
4326
+ }
3812
4327
  promoteNextBgQueuedToRunning() {
3813
4328
  if (this.state.messages.some(function(m) {
3814
4329
  return m.isPending && m.role === "assistant" && m.isBackgroundTask;
@@ -3934,6 +4449,29 @@ var ChatSession = class {
3934
4449
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
3935
4450
  else this.state.messages.push(msg);
3936
4451
  }
4452
+ /**
4453
+ * The server's OWN copy of this turn is already on screen.
4454
+ *
4455
+ * A first-page fetch can land between the server settling the item and this
4456
+ * poll's tick, and now that the local bubbles carry the item id
4457
+ * (_stampTurnWithItemId) the rescue correctly drops them and renders the
4458
+ * server's settled pair instead. There is then nothing left to resolve: the
4459
+ * -1 fallback would push the answer in a SECOND time at the bottom of the list,
4460
+ * and the positional fallbacks would hijack some other turn's bubble.
4461
+ *
4462
+ * The USER bubble counts, not just a settled assistant: an item whose answer is
4463
+ * empty produces no assistant bubble at all in the mapper, and that variant
4464
+ * would otherwise still bottom-push "No text response received...". While a turn
4465
+ * is genuinely live its user bubble is always pending (the queued branch sets
4466
+ * isPendingQueued, promoteNextQueuedToRunning sets isPendingInProcess), so this
4467
+ * cannot fire early.
4468
+ */
4469
+ _turnAlreadyRendered(serverId) {
4470
+ if (!serverId) return false;
4471
+ return this.state.messages.some(function(m) {
4472
+ return m._serverItemId === serverId && !m.isPending && !m.isPendingQueued && !m.isPendingInProcess;
4473
+ });
4474
+ }
3937
4475
  onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
3938
4476
  if (serverId) this.historyItemPolls.delete(serverId);
3939
4477
  if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
@@ -3942,6 +4480,12 @@ var ChatSession = class {
3942
4480
  if (serverId) this.cancelledServerIds.delete(serverId);
3943
4481
  return;
3944
4482
  }
4483
+ if (this._turnAlreadyRendered(serverId)) {
4484
+ if (serverId) this.cancelledServerIds.delete(serverId);
4485
+ this.host.notify();
4486
+ this.updateHistoryCache();
4487
+ return;
4488
+ }
3945
4489
  var targetIdx = this.resolveQueuedUserBubble(serverId);
3946
4490
  if (targetIdx === void 0) {
3947
4491
  this.host.notify();
@@ -3983,6 +4527,12 @@ var ChatSession = class {
3983
4527
  if (serverId) this.cancelledServerIds.delete(serverId);
3984
4528
  return;
3985
4529
  }
4530
+ if (this._turnAlreadyRendered(serverId)) {
4531
+ if (serverId) this.cancelledServerIds.delete(serverId);
4532
+ this.host.notify();
4533
+ this.updateHistoryCache();
4534
+ return;
4535
+ }
3986
4536
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
3987
4537
  if (isNotExists) {
3988
4538
  var userIdx = serverId ? this.state.messages.findIndex(function(m) {
@@ -5244,21 +5794,15 @@ var ChatSession = class {
5244
5794
  var rescued = [];
5245
5795
  for (var ri = 0; ri < self.state.messages.length; ri++) {
5246
5796
  var mm = self.state.messages[ri];
5247
- if (mm.isBackgroundTask) continue;
5248
- if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
5249
- if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
5250
- if (!mm._serverItemId) {
5251
- if (mm._stageId) {
5252
- rescued.push(mm);
5253
- continue;
5254
- }
5255
- if (mappedHasPendingAssistant) continue;
5256
- if (mm.isSendingToServer || mm.isPendingQueued || mm.isPendingInProcess || mm.isPending) rescued.push(mm);
5257
- else if (self.state.sending && mm.role === "user") {
5258
- var next = self.state.messages[ri + 1];
5259
- if (next && !next.isBackgroundTask && next.isPending && !next._serverItemId) rescued.push(mm);
5260
- }
5261
- }
5797
+ if (shouldRescueInFlightMessage(mm, {
5798
+ hasServerId: function(sid) {
5799
+ return !!serverIds[sid];
5800
+ },
5801
+ pageHasPendingAssistant: mappedHasPendingAssistant,
5802
+ sending: !!self.state.sending,
5803
+ next: self.state.messages[ri + 1],
5804
+ loadKey
5805
+ })) rescued.push(mm);
5262
5806
  }
5263
5807
  var oldestInPage1 = void 0;
5264
5808
  mapped.forEach(function(m) {
@@ -5309,6 +5853,9 @@ var ChatSession = class {
5309
5853
  keptOlderPages = prependOlder.length > 0 || interleave.length > 0;
5310
5854
  self.state.messages = prependOlder.length ? prependOlder.concat(page1) : page1;
5311
5855
  rescued.forEach(function(m) {
5856
+ if (m._serverItemId && self.state.messages.some(function(x) {
5857
+ return x._serverItemId === m._serverItemId && x.role === m.role;
5858
+ })) return;
5312
5859
  self.state.messages.push(m);
5313
5860
  });
5314
5861
  if (Object.keys(locallyCancelled).length) {
@@ -5433,6 +5980,7 @@ var ChatSession = class {
5433
5980
  releaseBgFlag();
5434
5981
  self.updateHistoryCache();
5435
5982
  self.host.notify();
5983
+ if (self.host.settleScroll) self.host.settleScroll();
5436
5984
  }, function() {
5437
5985
  releaseBgFlag();
5438
5986
  self.host.notify();
@@ -5460,8 +6008,8 @@ var ChatSession = class {
5460
6008
  if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
5461
6009
  if ((item._isBgTask || item._isOnBgQueue) && !bgAllow[item.id]) return;
5462
6010
  var capturedId = item.id;
5463
- var pp = item.poll({
5464
- latency: POLL_INTERVAL,
6011
+ var isBg = !!(item._isBgTask || item._isOnBgQueue);
6012
+ var pollOpts = {
5465
6013
  onResponse: function(response) {
5466
6014
  if (isPollStopped(response)) return;
5467
6015
  self.handleHistoryItemResolution(capturedId, response, platform);
@@ -5476,9 +6024,9 @@ var ChatSession = class {
5476
6024
  var uIdx = self.state.messages.findIndex(function(m) {
5477
6025
  return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
5478
6026
  });
5479
- var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
6027
+ var isBg2 = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
5480
6028
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
5481
- if (!isBg) {
6029
+ if (!isBg2) {
5482
6030
  if (uIdx !== -1) {
5483
6031
  var ex = self.state.messages[uIdx];
5484
6032
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
@@ -5505,7 +6053,8 @@ var ChatSession = class {
5505
6053
  self.updateHistoryCache();
5506
6054
  }
5507
6055
  }
5508
- });
6056
+ };
6057
+ var pp = isBg ? item.poll(Object.assign({ latency: POLL_INTERVAL }, pollOpts)) : self._fgPollWithEarlyProbe(item, capturedId, pollOpts);
5509
6058
  self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
5510
6059
  if (pp && pp.catch) pp.catch(function() {
5511
6060
  });
@@ -5513,7 +6062,7 @@ var ChatSession = class {
5513
6062
  self.drainBgTaskQueue();
5514
6063
  }
5515
6064
  if (!fetchMore) self.refreshLiveIndexState();
5516
- if (!fetchMore) return self.host.scrollToBottomIfSticky();
6065
+ if (!fetchMore) return self.host.settleScroll ? self.host.settleScroll() : self.host.scrollToBottomIfSticky();
5517
6066
  }).catch(function(err) {
5518
6067
  console.warn("[chat-engine] getChatHistory failed", err);
5519
6068
  }).then(function() {
@@ -6180,6 +6729,7 @@ exports.POLL_INTERVAL = POLL_INTERVAL;
6180
6729
  exports.PRESIGN_SAFETY_MARGIN_MS = PRESIGN_SAFETY_MARGIN_MS;
6181
6730
  exports.PREVIEWABLE_IMAGE_CONTENT_TYPES = PREVIEWABLE_IMAGE_CONTENT_TYPES;
6182
6731
  exports.PREVIEW_BROWSER_CACHE_SECONDS = PREVIEW_BROWSER_CACHE_SECONDS;
6732
+ exports.PREVIEW_LAYOUT_BOX_SELECTOR = PREVIEW_LAYOUT_BOX_SELECTOR;
6183
6733
  exports.PREVIEW_URL_EXPIRES_SECONDS = PREVIEW_URL_EXPIRES_SECONDS;
6184
6734
  exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
6185
6735
  exports.RTF_EXTS = RTF_EXTS;
@@ -6205,6 +6755,7 @@ exports.buildIndexingWindowMessage = buildIndexingWindowMessage;
6205
6755
  exports.callClaudeWithMcp = callClaudeWithMcp;
6206
6756
  exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
6207
6757
  exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
6758
+ exports.canonicalizePathForm = canonicalizePathForm;
6208
6759
  exports.chatEngineConfig = chatEngineConfig;
6209
6760
  exports.classifyInlineLink = classifyInlineLink;
6210
6761
  exports.clearAttachmentParsers = clearAttachmentParsers;
@@ -6214,6 +6765,7 @@ exports.configureChatEngine = configureChatEngine;
6214
6765
  exports.contentTypeForExt = contentTypeForExt;
6215
6766
  exports.createHistoryFiller = createHistoryFiller;
6216
6767
  exports.createInlineLinkRegex = createInlineLinkRegex;
6768
+ exports.createScrollAnchor = createScrollAnchor;
6217
6769
  exports.encodePathSegments = encodePathSegments;
6218
6770
  exports.encodingClassForExt = encodingClassForExt;
6219
6771
  exports.ensureHtmlCharset = ensureHtmlCharset;
@@ -6283,6 +6835,7 @@ exports.peekImagePreviewUrl = peekImagePreviewUrl;
6283
6835
  exports.prepareDownloadText = prepareDownloadText;
6284
6836
  exports.presignExpiryEpochMs = presignExpiryEpochMs;
6285
6837
  exports.previewImageContentType = previewImageContentType;
6838
+ exports.previewLayoutBox = previewLayoutBox;
6286
6839
  exports.previewMintCacheToken = previewMintCacheToken;
6287
6840
  exports.previewableExtOf = previewableExtOf;
6288
6841
  exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
@@ -6296,6 +6849,7 @@ exports.runIndexUniqueId = runIndexUniqueId;
6296
6849
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
6297
6850
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
6298
6851
  exports.setProjectContextWindow = setProjectContextWindow;
6852
+ exports.shouldRescueInFlightMessage = shouldRescueInFlightMessage;
6299
6853
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;
6300
6854
  exports.transformContentWithImages = transformContentWithImages;
6301
6855
  exports.transformContentWithOpenAIImages = transformContentWithOpenAIImages;