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.css +8 -0
- package/bunnyquery.js +508 -90
- package/dist/engine.cjs +446 -51
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +482 -212
- package/dist/engine.d.ts +482 -212
- package/dist/engine.mjs +442 -52
- package/dist/engine.mjs.map +1 -1
- package/package.json +3 -2
- package/src/engine/history.ts +64 -0
- package/src/engine/image_preview.ts +0 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/link_markup.ts +9 -1
- package/src/engine/links.ts +31 -3
- package/src/engine/scroll_anchor.ts +367 -0
- package/src/engine/session.ts +302 -60
- package/src/widget.css +8 -0
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
|
-
|
|
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
|
-
|
|
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 : "") + '"
|
|
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 =
|
|
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
|
-
|
|
3184
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
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
|
|
5322
|
-
|
|
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
|
|
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 (!
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8867
|
-
for (var
|
|
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
|
-
|
|
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 (
|
|
10045
|
+
if (CS.stickToBottom) {
|
|
9633
10046
|
box.scrollTop = box.scrollHeight;
|
|
9634
10047
|
return;
|
|
9635
10048
|
}
|
|
9636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
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;
|