bunnyquery 1.6.2 → 1.7.0

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.mjs CHANGED
@@ -288,7 +288,7 @@ File attachments: When a user message contains an "Attached files:" section with
288
288
  - Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
289
289
  - Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
290
290
  - For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
291
- File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](path/to/file) for storage paths, or [filename](https://...) for external URLs. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
291
+ File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix \u2014 do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand \u2014 so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
292
292
  File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records. Present each result as a markdown link as described above. Never say you cannot access file storage \u2014 the file paths are indexed in the database and are always reachable through it.
293
293
  File generation: When the user asks you to generate a file \u2014 or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown \u2014 put the file's full contents inside a fenced code block whose info string is the intended filename WITH its extension (e.g. report.csv), NOT a language name like "csv". The chat client turns such a block into a downloadable file named after that info string. Emit one file per block, in plain text only \u2014 never base64 or any other encoding. Example for CSV:
294
294
  \`\`\`filename.csv
@@ -413,6 +413,8 @@ ${placeholder}
413
413
 
414
414
  DATAFY this window: call postRecords and save records for everything in it - ONE RECORD PER ROW for tabular data (keyed by the column headers), or one record per section for prose. Capture every value you can read. Use the storage path above for the "src::" unique_id on the file-level record, and link every row/section record to it by reference.
415
415
 
416
+ If this window has PHOTOS attached as images, LOOK at each one and datafy what it actually shows into the record for the row it is anchored to (a \xABPHOTO A88\xBB marker in the grid text only says WHERE a picture sits - the picture itself is attached to this message). Never report that photo contents could not be extracted when images are attached here.
417
+
416
418
  Save records for THIS window only, then stop and report what you saved. Do NOT try to read the rest of the file, and do NOT call readFileContent - if more remains, the next window is read and sent to you automatically. Report only what you were actually shown, and never imply you have seen the whole file when the note beside the window says more remains.`;
417
419
  }
418
420
  function buildIndexingContinueMessage(attachment) {
@@ -523,8 +525,10 @@ function isAuthExpiredError(input) {
523
525
  var EXPIRED_ATTACHMENT_URL_HOST = "_expired_.url";
524
526
  var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
525
527
  var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
528
+ var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
529
+ var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
526
530
  function createInlineLinkRegex() {
527
- return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]+|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]+|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
531
+ return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
528
532
  }
529
533
  function safeDecodeURIComponent(v) {
530
534
  try {
@@ -584,18 +588,150 @@ function isServiceDbAttachmentHref(href, serviceId) {
584
588
  return false;
585
589
  }
586
590
  }
591
+ function readExpiredAttachmentHref(href) {
592
+ if (!href) return null;
593
+ try {
594
+ var parsed = new URL(href);
595
+ if (parsed.hostname !== EXPIRED_ATTACHMENT_URL_HOST) return null;
596
+ return normalizeAttachmentPathCandidate(parsed.pathname || "") || null;
597
+ } catch (e) {
598
+ return null;
599
+ }
600
+ }
587
601
  function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
588
602
  if (!content) return content;
589
603
  if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
590
604
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
591
- if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
605
+ if (!isServiceDbAttachmentHref(href, serviceId)) return _m;
592
606
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
593
- var labelPath = normalizeAttachmentPathCandidate(label);
594
- var fullPath = remotePath || labelPath;
595
- if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
607
+ var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
608
+ if (!fullPath) return _m;
596
609
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
597
610
  });
598
611
  }
612
+ function isHttpUrlLike(target) {
613
+ return /^https?:\/\//i.test((target || "").trim());
614
+ }
615
+ function repairUrlWhitespace(href) {
616
+ if (!href || !/\s/.test(href)) return href;
617
+ var stripped = href.replace(/\s+/g, "");
618
+ if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
619
+ return href.trim().replace(/\s/g, "%20");
620
+ }
621
+ function normalizeTrailingInlineToken(value) {
622
+ if (!value) return value;
623
+ var out = value.replace(/[.,;:!?]+$/, "");
624
+ var trimUnmatched = function(openCh, closeCh) {
625
+ while (out.charAt(out.length - 1) === closeCh) {
626
+ var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
627
+ var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
628
+ if (closeCount > openCount) out = out.slice(0, -1);
629
+ else break;
630
+ }
631
+ };
632
+ trimUnmatched("(", ")");
633
+ trimUnmatched("[", "]");
634
+ trimUnmatched("{", "}");
635
+ out = out.replace(/[`'"*>]+$/, "");
636
+ return out;
637
+ }
638
+ function classifyInlineLink(full, groups, ctx) {
639
+ var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
640
+ var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
641
+ var fresh = function(expiredHref) {
642
+ return ctx.resolveFreshHref ? ctx.resolveFreshHref(expiredHref) : void 0;
643
+ };
644
+ var isDbHost = function(url) {
645
+ return !!dbHostPrefix && url.toLowerCase().indexOf(dbHostPrefix) === 0;
646
+ };
647
+ var asStoredFile = function(remotePath2, label) {
648
+ if (!remotePath2) return null;
649
+ var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
650
+ var cached = fresh(expiredHref);
651
+ return {
652
+ part: {
653
+ type: "link",
654
+ label: truncateLabelForDisplay(label),
655
+ fullLabel: label,
656
+ href: cached || expiredHref,
657
+ expired: !cached,
658
+ expiredHref,
659
+ remotePath: remotePath2
660
+ }
661
+ };
662
+ };
663
+ if (g1) {
664
+ var rawPath = normalizeTrailingInlineToken(g1);
665
+ var tail = full.slice(("src::" + rawPath).length);
666
+ var srcIsUrl = isHttpUrlLike(rawPath);
667
+ if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
668
+ return {
669
+ part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
670
+ tail
671
+ };
672
+ }
673
+ var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.serviceId) || normalizeAttachmentPathCandidate(rawPath) : normalizeAttachmentPathCandidate(rawPath));
674
+ var srcBuilt = asStoredFile(srcPath, srcPath);
675
+ return srcBuilt ? { part: srcBuilt.part, tail } : null;
676
+ }
677
+ if (g4 && g5) {
678
+ var dbTarget = /^db:(.+)$/i.exec(g5.trim());
679
+ if (dbTarget) {
680
+ var declared = asStoredFile(normalizeAttachmentPathCandidate(dbTarget[1]), g4);
681
+ if (!declared) return null;
682
+ declared.part.label = truncateLabelForDisplay(g4);
683
+ declared.part.fullLabel = g4;
684
+ return declared;
685
+ }
686
+ if (isHttpUrlLike(g5)) {
687
+ return classifyInlineLink(full, [void 0, g4, repairUrlWhitespace(g5), void 0, void 0, void 0], ctx);
688
+ }
689
+ var trimmedTarget = g5.trim();
690
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedTarget) || trimmedTarget.charAt(0) === "#") {
691
+ return {
692
+ part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: trimmedTarget, expired: false }
693
+ };
694
+ }
695
+ var built = asStoredFile(normalizeAttachmentPathCandidate(g5), g4);
696
+ if (!built) return null;
697
+ built.part.label = truncateLabelForDisplay(g4);
698
+ built.part.fullLabel = g4;
699
+ return built;
700
+ }
701
+ var originalHref = g3 || g6 || "";
702
+ if (!originalHref) return null;
703
+ var urlTail;
704
+ if (!g3 && g6) {
705
+ var trimmedUrl = normalizeTrailingInlineToken(originalHref);
706
+ if (trimmedUrl !== originalHref) urlTail = originalHref.slice(trimmedUrl.length);
707
+ originalHref = trimmedUrl;
708
+ }
709
+ var withTail = function(r) {
710
+ return urlTail ? { part: r.part, tail: urlTail } : r;
711
+ };
712
+ var urlLabel = g2 || originalHref;
713
+ var carried = readExpiredAttachmentHref(originalHref);
714
+ if (carried) {
715
+ var carriedBuilt = asStoredFile(carried, g2 || carried);
716
+ if (carriedBuilt) {
717
+ if (g2) {
718
+ carriedBuilt.part.label = truncateLabelForDisplay(g2);
719
+ carriedBuilt.part.fullLabel = g2;
720
+ }
721
+ return withTail(carriedBuilt);
722
+ }
723
+ }
724
+ if (isServiceDbAttachmentHref(originalHref, ctx.serviceId)) {
725
+ var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.serviceId);
726
+ if (remotePath) {
727
+ var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
728
+ if (dbBuilt) return withTail(dbBuilt);
729
+ }
730
+ }
731
+ return withTail({
732
+ part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
733
+ });
734
+ }
599
735
  function truncateLabelForDisplay(label) {
600
736
  if (!label) return label;
601
737
  if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
@@ -681,7 +817,7 @@ var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
681
817
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
682
818
  var MCP_NAME = "BunnyQuery";
683
819
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
684
- var DEFAULT_OPENAI_MODEL = "gpt-5.4";
820
+ var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
685
821
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
686
822
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
687
823
  var getOpenAIImageDetail = (model) => {
@@ -1227,18 +1363,29 @@ function mapHistoryListToMessages(list, platform, opts) {
1227
1363
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1228
1364
  if (userText) {
1229
1365
  var displayContent;
1366
+ var indexFile = void 0;
1230
1367
  if (item._isBgTask) {
1231
1368
  var nameMatch = userText.match(/^- name: (.+)$/m);
1232
1369
  if (nameMatch) {
1233
1370
  var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1234
1371
  var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1235
1372
  var pathMatch = userText.match(/^- storage path: (.+)$/m);
1373
+ var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1236
1374
  displayContent = opts.formatIndexingLabel(
1237
1375
  nameMatch[1].trim(),
1238
1376
  mimeMatch ? mimeMatch[1].trim() : "",
1239
1377
  sizeMatch ? Number(sizeMatch[1]) : null,
1240
- pathMatch ? pathMatch[1].trim() : void 0
1378
+ pathMatch ? pathMatch[1].trim() : void 0,
1379
+ false,
1380
+ isContinuePass
1241
1381
  );
1382
+ indexFile = {
1383
+ name: nameMatch[1].trim(),
1384
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1385
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1386
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1387
+ continued: isContinuePass
1388
+ };
1242
1389
  } else {
1243
1390
  displayContent = userText;
1244
1391
  }
@@ -1250,6 +1397,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1250
1397
  if (isQueued) userMsg.isPendingQueued = true;
1251
1398
  if (isCancelledItem) userMsg.isCancelled = true;
1252
1399
  if (item._isBgTask) userMsg.isBackgroundTask = true;
1400
+ if (indexFile) userMsg._indexFile = indexFile;
1253
1401
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1254
1402
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1255
1403
  mapped.push(userMsg);
@@ -1277,6 +1425,91 @@ function mapHistoryListToMessages(list, platform, opts) {
1277
1425
  return { messages: mapped, runningItemIds };
1278
1426
  }
1279
1427
 
1428
+ // src/engine/viewport_fill.ts
1429
+ var HISTORY_FILL_SLACK_PX = 64;
1430
+ var MAX_HISTORY_FILL_PAGES = 24;
1431
+ var IDLE_WAIT_STEP_MS = 120;
1432
+ var IDLE_WAIT_MAX_MS = 15e3;
1433
+ async function waitForIdle(opts, stale) {
1434
+ var waited = 0;
1435
+ while (opts.isLoading()) {
1436
+ if (stale() || waited >= IDLE_WAIT_MAX_MS) return false;
1437
+ await new Promise(function(r) {
1438
+ setTimeout(r, IDLE_WAIT_STEP_MS);
1439
+ });
1440
+ waited += IDLE_WAIT_STEP_MS;
1441
+ }
1442
+ return !stale();
1443
+ }
1444
+ async function fillHistoryViewport(opts) {
1445
+ var maxPages = typeof opts.maxPages === "number" ? opts.maxPages : MAX_HISTORY_FILL_PAGES;
1446
+ var stale = function() {
1447
+ return !!(opts.isStale && opts.isStale());
1448
+ };
1449
+ var swallowed = 0;
1450
+ for (var page = 0; page < maxPages; page++) {
1451
+ if (stale() || opts.isEndOfList()) return;
1452
+ if (!await waitForIdle(opts, stale)) return;
1453
+ var satisfied = false;
1454
+ try {
1455
+ satisfied = !!await opts.isSatisfied();
1456
+ } catch {
1457
+ return;
1458
+ }
1459
+ if (satisfied || stale()) return;
1460
+ if (!await waitForIdle(opts, stale)) return;
1461
+ var before = opts.messageCount();
1462
+ var attempted;
1463
+ try {
1464
+ attempted = await opts.fetchOlder();
1465
+ } catch {
1466
+ return;
1467
+ }
1468
+ if (stale()) return;
1469
+ if (attempted === false) {
1470
+ if (++swallowed > 3) return;
1471
+ page--;
1472
+ continue;
1473
+ }
1474
+ if (opts.messageCount() <= before) return;
1475
+ }
1476
+ }
1477
+ function createHistoryFiller(base) {
1478
+ var pending = [];
1479
+ var running = false;
1480
+ async function allSatisfied() {
1481
+ var next = [];
1482
+ for (var i = 0; i < pending.length; i++) {
1483
+ if (!await pending[i]()) next.push(pending[i]);
1484
+ }
1485
+ pending = next;
1486
+ return pending.length === 0;
1487
+ }
1488
+ return {
1489
+ isRunning: function() {
1490
+ return running;
1491
+ },
1492
+ fill: function(isSatisfied) {
1493
+ pending.push(isSatisfied);
1494
+ if (running) return Promise.resolve();
1495
+ running = true;
1496
+ var done = function() {
1497
+ running = false;
1498
+ pending = [];
1499
+ };
1500
+ return fillHistoryViewport({
1501
+ isSatisfied: allSatisfied,
1502
+ isEndOfList: base.isEndOfList,
1503
+ isLoading: base.isLoading,
1504
+ messageCount: base.messageCount,
1505
+ fetchOlder: base.fetchOlder,
1506
+ isStale: base.isStale,
1507
+ maxPages: base.maxPages
1508
+ }).then(done, done);
1509
+ }
1510
+ };
1511
+ }
1512
+
1280
1513
  // src/engine/session.ts
1281
1514
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1282
1515
  function nowMs() {
@@ -1314,6 +1547,7 @@ var ChatSession = class {
1314
1547
  };
1315
1548
  this.bgTaskQueue = [];
1316
1549
  this.cancelledServerIds = /* @__PURE__ */ new Set();
1550
+ this.cancelledIndexKeys = /* @__PURE__ */ new Set();
1317
1551
  this.pendingAgentRequests = {};
1318
1552
  this.aiChatHistoryCache = {};
1319
1553
  this.historyItemPolls = /* @__PURE__ */ new Map();
@@ -1336,6 +1570,23 @@ var ChatSession = class {
1336
1570
  this.historyItemPolls.set(id, { kind, stop });
1337
1571
  return p;
1338
1572
  }
1573
+ /**
1574
+ * Stop and forget one item's poll. Used after a cancel: the row is either gone
1575
+ * (cancelled while queued) or flagged cancelled (cancelled while running), so
1576
+ * asking about it again only burns requests. Safe when no poll is attached, and
1577
+ * safe on an older skapi-js with no stop handle (the entry is then LEFT in the
1578
+ * map so a later drain cannot stack a second, unstoppable poll on the id).
1579
+ */
1580
+ _stopPoll(id) {
1581
+ var handle = this.historyItemPolls.get(id);
1582
+ if (!handle) return;
1583
+ if (typeof handle.stop !== "function") return;
1584
+ try {
1585
+ handle.stop();
1586
+ } catch (e) {
1587
+ }
1588
+ this.historyItemPolls.delete(id);
1589
+ }
1339
1590
  /** True while any pause reason is active. */
1340
1591
  isPollingPaused() {
1341
1592
  return this._pauseReasons.size > 0;
@@ -1438,7 +1689,18 @@ var ChatSession = class {
1438
1689
  if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1439
1690
  msgs[thIdx] = reply;
1440
1691
  } else {
1441
- msgs.push(reply);
1692
+ var dupIdx = -1;
1693
+ if (serverId) {
1694
+ for (var d = msgs.length - 1; d >= 0; d--) {
1695
+ var dm = msgs[d];
1696
+ if (dm && dm.role === "assistant" && dm._serverItemId === serverId) {
1697
+ dupIdx = d;
1698
+ break;
1699
+ }
1700
+ }
1701
+ }
1702
+ if (dupIdx !== -1) msgs[dupIdx] = reply;
1703
+ else msgs.push(reply);
1442
1704
  }
1443
1705
  for (var j = 0; j < msgs.length; j++) {
1444
1706
  var u = msgs[j];
@@ -1678,7 +1940,7 @@ var ChatSession = class {
1678
1940
  self.state.sending = false;
1679
1941
  if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1680
1942
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1681
- self.host.scrollToBottom(true);
1943
+ self.host.scrollToBottomIfSticky(true);
1682
1944
  });
1683
1945
  });
1684
1946
  }
@@ -1692,6 +1954,7 @@ var ChatSession = class {
1692
1954
  if (nextIdx === -1) return;
1693
1955
  var existing = this.state.messages[nextIdx];
1694
1956
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1957
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
1695
1958
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1696
1959
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1697
1960
  this.state.messages[nextIdx] = promoted;
@@ -1712,6 +1975,7 @@ var ChatSession = class {
1712
1975
  var existing = this.state.messages[nextIdx];
1713
1976
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1714
1977
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1978
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
1715
1979
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1716
1980
  if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1717
1981
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
@@ -1812,7 +2076,7 @@ var ChatSession = class {
1812
2076
  this.promoteNextQueuedToRunning();
1813
2077
  this.updateHistoryCache();
1814
2078
  this.host.notify();
1815
- this.host.scrollToBottom(true);
2079
+ this.host.scrollToBottomIfSticky(true);
1816
2080
  }
1817
2081
  onQueuedSendError(_composed, err, serverId, ownerKey) {
1818
2082
  if (serverId) this.historyItemPolls.delete(serverId);
@@ -1862,7 +2126,7 @@ var ChatSession = class {
1862
2126
  this.promoteNextQueuedToRunning();
1863
2127
  this.updateHistoryCache();
1864
2128
  this.host.notify();
1865
- this.host.scrollToBottom(true);
2129
+ this.host.scrollToBottomIfSticky(true);
1866
2130
  return;
1867
2131
  }
1868
2132
  var targetIdx = this.resolveQueuedUserBubble(serverId);
@@ -1876,7 +2140,7 @@ var ChatSession = class {
1876
2140
  this.promoteNextQueuedToRunning();
1877
2141
  this.updateHistoryCache();
1878
2142
  this.host.notify();
1879
- this.host.scrollToBottom(true);
2143
+ this.host.scrollToBottomIfSticky(true);
1880
2144
  }
1881
2145
  cancelQueuedMessage(msg, idx) {
1882
2146
  var self = this;
@@ -1900,6 +2164,7 @@ var ChatSession = class {
1900
2164
  })).then(function(result) {
1901
2165
  if (result && result.removed) {
1902
2166
  self.cancelledServerIds.add(serverId);
2167
+ self._stopPoll(serverId);
1903
2168
  var qi = self.bgTaskQueue.findIndex(function(e) {
1904
2169
  return e.id === serverId;
1905
2170
  });
@@ -1908,7 +2173,13 @@ var ChatSession = class {
1908
2173
  return m._serverItemId === serverId && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1909
2174
  });
1910
2175
  if (removeIdx !== -1) {
1911
- self.state.messages[removeIdx] = { role: "user", content: self.state.messages[removeIdx].content, isCancelled: true, _serverItemId: serverId };
2176
+ var wasMsg = self.state.messages[removeIdx];
2177
+ var cancelledMsg = { role: "user", content: wasMsg.content, isCancelled: true, _serverItemId: serverId };
2178
+ if (wasMsg.isBackgroundTask) cancelledMsg.isBackgroundTask = true;
2179
+ if (wasMsg._indexFile) cancelledMsg._indexFile = wasMsg._indexFile;
2180
+ if (wasMsg._useBgQueue) cancelledMsg._useBgQueue = true;
2181
+ if (wasMsg._ownerKey !== void 0) cancelledMsg._ownerKey = wasMsg._ownerKey;
2182
+ self.state.messages[removeIdx] = cancelledMsg;
1912
2183
  var thById = self.state.messages.findIndex(function(m) {
1913
2184
  return m._serverItemId === serverId && m.isPending && m.role === "assistant";
1914
2185
  });
@@ -1945,6 +2216,42 @@ var ChatSession = class {
1945
2216
  }
1946
2217
  });
1947
2218
  }
2219
+ /**
2220
+ * Stop indexing a file, from its collapsed row — every pass at once, not just
2221
+ * the bubble the user happens to see.
2222
+ *
2223
+ * A big file is indexed as a CHAIN of passes, so cancelling only the live one
2224
+ * accomplishes nothing: the next pass is dispatched as soon as it settles.
2225
+ * Three things end the chain:
2226
+ * 1. every queued/running pass of this file is cancelled server-side
2227
+ * (csr-cancel deletes a queued row and flags a running one "cancelled",
2228
+ * which is also the worker's gate for NOT enqueueing the next window);
2229
+ * 2. the file is remembered in cancelledIndexKeys, so the client-driven
2230
+ * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
2231
+ * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
2232
+ * drain rather than surfacing a fresh "Indexing…" bubble.
2233
+ *
2234
+ * Records already written by the passes that DID run are kept — this stops the
2235
+ * work, it does not undo it.
2236
+ */
2237
+ cancelIndexingGroup(group) {
2238
+ var self = this;
2239
+ if (!group || !group.key) return;
2240
+ var scoped = this.getHistoryCacheKey() + "|" + group.key;
2241
+ this.cancelledIndexKeys.add(scoped);
2242
+ var ids = group.cancellableIds || [];
2243
+ if (!ids.length) {
2244
+ this.host.notify();
2245
+ return;
2246
+ }
2247
+ ids.forEach(function(serverId) {
2248
+ var idx = self.state.messages.findIndex(function(m) {
2249
+ return m._serverItemId === serverId && m.role === "user" && (m.isPendingQueued || m.isPendingInProcess);
2250
+ });
2251
+ if (idx === -1) return;
2252
+ self.cancelQueuedMessage(self.state.messages[idx], idx);
2253
+ });
2254
+ }
1948
2255
  // --- typewriter -------------------------------------------------------
1949
2256
  // Reveal `fullText` into a message bubble at a constant wall-clock RATE
1950
2257
  // (chars/second) driven by requestAnimationFrame, rather than a fixed number
@@ -2131,6 +2438,7 @@ var ChatSession = class {
2131
2438
  var u = this.state.messages[uIdx];
2132
2439
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
2133
2440
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2441
+ if (u._indexFile) cleaned._indexFile = u._indexFile;
2134
2442
  this.state.messages[uIdx] = cleaned;
2135
2443
  }
2136
2444
  // If an immediate-send request for the current cache key is still in flight
@@ -2148,13 +2456,13 @@ var ChatSession = class {
2148
2456
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
2149
2457
  })) return Promise.resolve();
2150
2458
  this.state.sending = true;
2151
- this.host.scrollToBottom(true);
2459
+ this.host.scrollToBottomIfSticky(true);
2152
2460
  return Promise.resolve(pending).catch(function() {
2153
2461
  }).then(function() {
2154
2462
  if (token !== self.state.gateRefreshToken) return;
2155
2463
  self.state.sending = false;
2156
2464
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
2157
- self.host.scrollToBottom(true);
2465
+ self.host.scrollToBottomIfSticky(true);
2158
2466
  });
2159
2467
  });
2160
2468
  }
@@ -2173,8 +2481,17 @@ var ChatSession = class {
2173
2481
  });
2174
2482
  if (idx !== -1) {
2175
2483
  this._clearPendingUserBubble(itemId);
2484
+ var wasBgTask = !!this.state.messages[idx].isBackgroundTask;
2176
2485
  if (isErr) {
2177
2486
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2487
+ if (wasBgTask) this.state.messages[idx].isBackgroundTask = true;
2488
+ this.host.notify();
2489
+ this.updateHistoryCache();
2490
+ return;
2491
+ }
2492
+ var text = answer || "No text response received from AI provider.";
2493
+ if (wasBgTask) {
2494
+ this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
2178
2495
  this.host.notify();
2179
2496
  this.updateHistoryCache();
2180
2497
  return;
@@ -2182,7 +2499,7 @@ var ChatSession = class {
2182
2499
  var lid = this._newLocalId();
2183
2500
  this.state.messages[idx] = { role: "assistant", content: "", _localId: lid, _serverItemId: itemId };
2184
2501
  this.host.notify();
2185
- this.enqueueTypewrite(idx, answer || "No text response received from AI provider.", lid);
2502
+ this.enqueueTypewrite(idx, text, lid);
2186
2503
  this.updateHistoryCache();
2187
2504
  return;
2188
2505
  }
@@ -2191,25 +2508,124 @@ var ChatSession = class {
2191
2508
  });
2192
2509
  if (userIdx === -1) return;
2193
2510
  var ex = this.state.messages[userIdx];
2194
- this.state.messages[userIdx] = { role: "user", content: ex.content, _serverItemId: itemId };
2511
+ var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2512
+ if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2513
+ if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2514
+ if (ex._useBgQueue) settledUser._useBgQueue = true;
2515
+ this.state.messages[userIdx] = settledUser;
2195
2516
  if (isErr) {
2196
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: answer, isError: true, _serverItemId: itemId });
2517
+ var errReply = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2518
+ if (ex.isBackgroundTask) errReply.isBackgroundTask = true;
2519
+ this.state.messages.splice(userIdx + 1, 0, errReply);
2520
+ this.host.notify();
2521
+ this.updateHistoryCache();
2522
+ return;
2523
+ }
2524
+ var text2 = answer || "No text response received from AI provider.";
2525
+ if (ex.isBackgroundTask) {
2526
+ this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
2197
2527
  this.host.notify();
2198
2528
  this.updateHistoryCache();
2199
2529
  return;
2200
2530
  }
2201
2531
  var lid2 = this._newLocalId();
2202
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId });
2532
+ var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
2533
+ this.state.messages.splice(userIdx + 1, 0, reply);
2203
2534
  this.host.notify();
2204
- this.enqueueTypewrite(userIdx + 1, answer || "No text response received from AI provider.", lid2);
2535
+ this.enqueueTypewrite(userIdx + 1, text2, lid2);
2205
2536
  this.updateHistoryCache();
2206
2537
  }
2538
+ /** How a bg task maps onto a collapsed row: the row's own key (storage path
2539
+ * when known, else the filename), scoped to the chat it belongs to. A storage
2540
+ * path is project-relative ("report.xlsx"), and ONE ChatSession serves every
2541
+ * project — unscoped, stopping a file in one project would silently suppress
2542
+ * the same filename's continuations in another. */
2543
+ _indexKeyOf(entry) {
2544
+ if (!entry) return "";
2545
+ var file = entry.storagePath || entry.filename;
2546
+ if (!file) return "";
2547
+ return entry.serviceId + "#" + entry.platform + "|" + file;
2548
+ }
2549
+ /**
2550
+ * Reconcile the bg queue with the files the user has stopped.
2551
+ *
2552
+ * A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
2553
+ * Reindex from the file manager — so it LIFTS the stop: the key is a storage
2554
+ * path, and without this an earlier cancel would silently kill every future
2555
+ * index of the same path. A continuation of a stopped file is dropped instead,
2556
+ * covering the pass that was dispatched in the moment before the cancel landed.
2557
+ */
2558
+ _applyIndexCancellations() {
2559
+ if (!this.cancelledIndexKeys.size) return;
2560
+ for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
2561
+ var entry = this.bgTaskQueue[i];
2562
+ var key = this._indexKeyOf(entry);
2563
+ if (!key || !this.cancelledIndexKeys.has(key)) continue;
2564
+ if (!entry.resumePass) {
2565
+ this.cancelledIndexKeys.delete(key);
2566
+ continue;
2567
+ }
2568
+ this.bgTaskQueue.splice(i, 1);
2569
+ this._stopPoll(entry.id);
2570
+ this._cancelServerItem(entry.id);
2571
+ }
2572
+ }
2573
+ /**
2574
+ * Cancel any live pass of a stopped file that turned up on its own.
2575
+ *
2576
+ * The client is not the only thing that continues a file: for PDFs and (when
2577
+ * windowed indexing is on) text/grid files the WORKER enqueues the next window
2578
+ * itself, and that pass reaches the chat through the history poll, never
2579
+ * through bgTaskQueue. The worker's own gate stops the chain when the running
2580
+ * row is cancelled — but if the row had already finished when the user hit
2581
+ * stop, the next window was queued a moment earlier and still arrives. Stop it
2582
+ * here rather than making the user hit stop again.
2583
+ *
2584
+ * Runs from drainBgTaskQueue, which both clients call after a history load.
2585
+ */
2586
+ _sweepCancelledIndexing() {
2587
+ if (!this.cancelledIndexKeys.size) return;
2588
+ var self = this;
2589
+ var chatKey = this.getHistoryCacheKey();
2590
+ var targets = [];
2591
+ this.state.messages.forEach(function(m, i) {
2592
+ if (!m.isBackgroundTask || m.role !== "user" || !m._serverItemId) return;
2593
+ if (m._cancelling || m.isSendingToServer) return;
2594
+ if (!(m.isPendingQueued || m.isPendingInProcess)) return;
2595
+ var ref = m._indexFile;
2596
+ var file = ref && (ref.path || ref.name);
2597
+ if (!file || !self.cancelledIndexKeys.has(chatKey + "|" + file)) return;
2598
+ targets.push({ msg: m, idx: i });
2599
+ });
2600
+ targets.forEach(function(t) {
2601
+ var idx = self.state.messages.indexOf(t.msg);
2602
+ self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2603
+ });
2604
+ }
2605
+ /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2606
+ * cancelQueuedMessage, which drives one, has nothing to act on). */
2607
+ _cancelServerItem(serverId) {
2608
+ var id = this.host.getIdentity();
2609
+ if (!serverId || id.platform !== "claude" && id.platform !== "openai") return;
2610
+ var url = id.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
2611
+ Promise.resolve(this.host.cancelRequest({
2612
+ url,
2613
+ method: "POST",
2614
+ id: serverId,
2615
+ queue: (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX,
2616
+ service: id.serviceId,
2617
+ owner: id.owner
2618
+ })).catch(function() {
2619
+ });
2620
+ }
2207
2621
  // Inject "Indexing: <file>" bubbles for queued bg tasks + attach their polls.
2208
2622
  drainBgTaskQueue() {
2209
2623
  var self = this;
2210
2624
  var id = this.host.getIdentity();
2211
2625
  var svcId = id.serviceId, plat = id.platform;
2212
2626
  if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
2627
+ this._applyIndexCancellations();
2628
+ this._sweepCancelledIndexing();
2213
2629
  var presentIds = {};
2214
2630
  var pendingIds = {};
2215
2631
  this.state.messages.forEach(function(m) {
@@ -2227,7 +2643,22 @@ var ChatSession = class {
2227
2643
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
2228
2644
  if (!presentIds[entry.id]) {
2229
2645
  var isRunning = entry.status === "running";
2230
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
2646
+ var userBubble = {
2647
+ role: "user",
2648
+ content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex, !!entry.resumePass),
2649
+ isBackgroundTask: true,
2650
+ _serverItemId: entry.id,
2651
+ // Structured ref so this live pass groups with the same file's passes
2652
+ // rebuilt from history (see indexing_groups.buildChatDisplayList).
2653
+ _indexFile: {
2654
+ name: entry.filename,
2655
+ path: entry.storagePath,
2656
+ mime: entry.mime,
2657
+ size: entry.size,
2658
+ isReindex: !!entry.isReindex,
2659
+ continued: !!entry.resumePass
2660
+ }
2661
+ };
2231
2662
  if (isRunning) userBubble.isPendingInProcess = true;
2232
2663
  else userBubble.isPendingQueued = true;
2233
2664
  self.state.messages.push(userBubble);
@@ -2237,7 +2668,7 @@ var ChatSession = class {
2237
2668
  presentIds[entry.id] = true;
2238
2669
  self.host.notify();
2239
2670
  self.updateHistoryCache();
2240
- self.host.scrollToBottom(false);
2671
+ self.host.scrollToBottomIfSticky(false);
2241
2672
  }
2242
2673
  if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
2243
2674
  var capturedId = entry.id, capturedPlat = plat;
@@ -2255,15 +2686,23 @@ var ChatSession = class {
2255
2686
  }).catch(function(err) {
2256
2687
  self.historyItemPolls.delete(capturedId);
2257
2688
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
2689
+ self._clearPendingUserBubble(capturedId);
2258
2690
  var bi = self.state.messages.findIndex(function(m) {
2259
2691
  return m.isPending && m._serverItemId === capturedId;
2260
2692
  });
2261
2693
  if (bi !== -1) {
2262
2694
  if (isNotExists) self.state.messages.splice(bi, 1);
2263
2695
  else self.state.messages[bi] = { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId };
2264
- self.host.notify();
2265
- self.updateHistoryCache();
2696
+ } else if (!isNotExists) {
2697
+ var ui = self.state.messages.findIndex(function(m) {
2698
+ return m.role === "user" && m._serverItemId === capturedId;
2699
+ });
2700
+ if (ui !== -1) {
2701
+ self.state.messages.splice(ui + 1, 0, { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId });
2702
+ }
2266
2703
  }
2704
+ self.host.notify();
2705
+ self.updateHistoryCache();
2267
2706
  }).then(function() {
2268
2707
  if (wasStopped) return;
2269
2708
  var qi = self.bgTaskQueue.findIndex(function(q) {
@@ -2291,8 +2730,10 @@ var ChatSession = class {
2291
2730
  var self = this;
2292
2731
  try {
2293
2732
  if (!entry || !entry.storagePath) return;
2733
+ if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
2294
2734
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2295
2735
  if (isImageVisionFile(entry.filename, entry.mime)) return;
2736
+ if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
2296
2737
  if (isErrorResponseBody(response)) return;
2297
2738
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
2298
2739
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
@@ -2385,6 +2826,7 @@ var ChatSession = class {
2385
2826
  serviceId: id.serviceId,
2386
2827
  formatIndexingLabel: self.host.formatIndexingLabel
2387
2828
  }).messages;
2829
+ var keptOlderPages = false;
2388
2830
  if (fetchMore) {
2389
2831
  self.state.messages = mapped.concat(self.state.messages);
2390
2832
  } else {
@@ -2395,7 +2837,12 @@ var ChatSession = class {
2395
2837
  });
2396
2838
  var locallyCancelled = {};
2397
2839
  self.state.messages.forEach(function(m) {
2398
- if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
2840
+ if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
2841
+ });
2842
+ var inFlightCancel = {};
2843
+ self.state.messages.forEach(function(m) {
2844
+ if (!m._serverItemId) return;
2845
+ if (m._cancelling || m._cancelError) inFlightCancel[m._serverItemId] = m;
2399
2846
  });
2400
2847
  var mappedHasPendingAssistant = mapped.some(function(m) {
2401
2848
  return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
@@ -2415,7 +2862,22 @@ var ChatSession = class {
2415
2862
  }
2416
2863
  }
2417
2864
  }
2418
- self.state.messages = mapped;
2865
+ var oldestInPage1 = void 0;
2866
+ mapped.forEach(function(m) {
2867
+ var sid = m._serverItemId;
2868
+ if (typeof sid !== "string") return;
2869
+ if (oldestInPage1 === void 0 || sid < oldestInPage1) oldestInPage1 = sid;
2870
+ });
2871
+ var sharesPage1 = self.state.messages.some(function(m) {
2872
+ return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
2873
+ });
2874
+ var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
2875
+ if (typeof m._serverItemId !== "string") return false;
2876
+ if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
2877
+ return m._serverItemId < oldestInPage1;
2878
+ });
2879
+ keptOlderPages = retainedOlder.length > 0;
2880
+ self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
2419
2881
  rescued.forEach(function(m) {
2420
2882
  self.state.messages.push(m);
2421
2883
  });
@@ -2423,19 +2885,38 @@ var ChatSession = class {
2423
2885
  for (var ci = 0; ci < self.state.messages.length; ci++) {
2424
2886
  var c = self.state.messages[ci];
2425
2887
  if (!c._serverItemId || !locallyCancelled[c._serverItemId] || c.isCancelled) continue;
2426
- self.state.messages[ci] = { role: "user", content: c.content, isCancelled: true, _serverItemId: c._serverItemId };
2888
+ self.state.messages[ci] = {
2889
+ role: "user",
2890
+ content: c.content,
2891
+ isCancelled: true,
2892
+ _serverItemId: c._serverItemId,
2893
+ isBackgroundTask: c.isBackgroundTask,
2894
+ _indexFile: c._indexFile,
2895
+ _useBgQueue: c._useBgQueue,
2896
+ _ownerKey: c._ownerKey
2897
+ };
2427
2898
  if (ci + 1 < self.state.messages.length && self.state.messages[ci + 1].isPending && self.state.messages[ci + 1]._serverItemId === c._serverItemId) {
2428
2899
  self.state.messages.splice(ci + 1, 1);
2429
2900
  }
2430
2901
  }
2431
2902
  }
2903
+ for (var fi = 0; fi < self.state.messages.length; fi++) {
2904
+ var fm = self.state.messages[fi];
2905
+ var was = fm._serverItemId && inFlightCancel[fm._serverItemId];
2906
+ if (!was || fm.isCancelled) continue;
2907
+ if (!(fm.isPendingQueued || fm.isPendingInProcess || fm.isPending)) continue;
2908
+ if (was._cancelling) fm._cancelling = true;
2909
+ if (was._cancelError) fm._cancelError = was._cancelError;
2910
+ }
2432
2911
  }
2433
- self.state.historyEndOfList = !!(history && history.endOfList);
2434
- self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2435
- var clearedAt = self.host.getClearedAt();
2436
- if (clearedAt && chatList.length > 0) {
2437
- var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2438
- if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2912
+ if (!keptOlderPages) {
2913
+ self.state.historyEndOfList = !!(history && history.endOfList);
2914
+ self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2915
+ var clearedAt = self.host.getClearedAt();
2916
+ if (clearedAt && chatList.length > 0) {
2917
+ var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2918
+ if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2919
+ }
2439
2920
  }
2440
2921
  if (self.state.historyRequestToken === token) {
2441
2922
  self.state.loadingHistory = false;
@@ -2464,18 +2945,25 @@ var ChatSession = class {
2464
2945
  return m.isPending && m._serverItemId === capturedId;
2465
2946
  });
2466
2947
  if (isNotExists) {
2467
- var isBg = aIdx !== -1 ? !!self.state.messages[aIdx].isBackgroundTask : false;
2948
+ var uIdx = self.state.messages.findIndex(function(m) {
2949
+ return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2950
+ });
2951
+ var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
2468
2952
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
2469
2953
  if (!isBg) {
2470
- var uIdx = self.state.messages.findIndex(function(m) {
2471
- return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2472
- });
2473
2954
  if (uIdx !== -1) {
2474
2955
  var ex = self.state.messages[uIdx];
2475
2956
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
2476
2957
  }
2477
2958
  self.cancelledServerIds.delete(capturedId);
2478
2959
  self.promoteNextQueuedToRunning();
2960
+ } else if (uIdx !== -1) {
2961
+ var bex = self.state.messages[uIdx];
2962
+ var bcancelled = { role: "user", content: bex.content, isCancelled: true, isBackgroundTask: true, _serverItemId: bex._serverItemId };
2963
+ if (bex._indexFile) bcancelled._indexFile = bex._indexFile;
2964
+ if (bex._useBgQueue) bcancelled._useBgQueue = true;
2965
+ self.state.messages[uIdx] = bcancelled;
2966
+ self.promoteNextBgQueuedToRunning();
2479
2967
  }
2480
2968
  self.host.notify();
2481
2969
  self.updateHistoryCache();
@@ -2496,7 +2984,7 @@ var ChatSession = class {
2496
2984
  });
2497
2985
  self.drainBgTaskQueue();
2498
2986
  }
2499
- if (!fetchMore) return self.host.scrollToBottom();
2987
+ if (!fetchMore) return self.host.scrollToBottomIfSticky();
2500
2988
  }).catch(function(err) {
2501
2989
  console.warn("[chat-engine] getChatHistory failed", err);
2502
2990
  }).then(function() {
@@ -2506,6 +2994,7 @@ var ChatSession = class {
2506
2994
  self.state.loadingOlderHistory = false;
2507
2995
  if (wasLoading) self.host.notify();
2508
2996
  }
2997
+ if (self.host.onHistoryLoaded) self.host.onHistoryLoaded(!!fetchMore, token);
2509
2998
  });
2510
2999
  }
2511
3000
  // --- attachment upload orchestration ---------------------------------
@@ -2703,6 +3192,189 @@ var ChatSession = class {
2703
3192
  }
2704
3193
  };
2705
3194
 
2706
- export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
3195
+ // src/engine/indexing_groups.ts
3196
+ var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
3197
+ var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
3198
+ function parseIndexingLabel(content) {
3199
+ if (typeof content !== "string" || !content) return null;
3200
+ var firstLine = content.split("\n")[0].trim();
3201
+ var m = firstLine.match(INDEXING_LABEL_RE);
3202
+ if (!m) return null;
3203
+ var head = m[3].split(" \xB7 ")[0].trim();
3204
+ var link = head.match(LEADING_MD_LINK_RE);
3205
+ var name = link ? link[1].trim() : head;
3206
+ if (!name) return null;
3207
+ return {
3208
+ name,
3209
+ path: link ? link[2].trim() : void 0,
3210
+ continued: !!m[2],
3211
+ isReindex: !!m[1]
3212
+ };
3213
+ }
3214
+ function readFileRef(msg) {
3215
+ var ref = msg && msg._indexFile;
3216
+ if (ref && (ref.path || ref.name)) {
3217
+ return {
3218
+ name: ref.name || ref.path || "",
3219
+ path: ref.path,
3220
+ mime: ref.mime,
3221
+ size: ref.size,
3222
+ isReindex: ref.isReindex,
3223
+ continued: !!ref.continued
3224
+ };
3225
+ }
3226
+ var parsed = parseIndexingLabel(msg && msg.content);
3227
+ if (!parsed) return null;
3228
+ return {
3229
+ name: parsed.name,
3230
+ path: parsed.path,
3231
+ isReindex: parsed.isReindex,
3232
+ continued: parsed.continued
3233
+ };
3234
+ }
3235
+ function isPendingMsg(m) {
3236
+ return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
3237
+ }
3238
+ function buildChatDisplayList(messages, opts) {
3239
+ var list = Array.isArray(messages) ? messages : [];
3240
+ var hasMoreHistory = !!(opts && opts.hasMoreHistory);
3241
+ var groups = {};
3242
+ var order = [];
3243
+ var runOfIndex = new Array(list.length);
3244
+ var runByItemId = {};
3245
+ var keyByName = {};
3246
+ var openRunOfKey = {};
3247
+ var runsOfKey = {};
3248
+ var keyOfRun = {};
3249
+ var runSeq = 0;
3250
+ for (var i = 0; i < list.length; i++) {
3251
+ var msg = list[i];
3252
+ if (!msg || !msg.isBackgroundTask) continue;
3253
+ var runId;
3254
+ var ref = msg.role === "user" ? readFileRef(msg) : null;
3255
+ if (ref) {
3256
+ var key = ref.path || keyByName[ref.name] || ref.name;
3257
+ if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
3258
+ runId = openRunOfKey[key];
3259
+ if (!runId) {
3260
+ runId = "run" + runSeq++;
3261
+ openRunOfKey[key] = runId;
3262
+ keyOfRun[runId] = key;
3263
+ (runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
3264
+ }
3265
+ } else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
3266
+ runId = runByItemId[msg._serverItemId];
3267
+ } else if (msg.role !== "user") {
3268
+ runId = runOfIndex[i - 1];
3269
+ }
3270
+ if (!runId) continue;
3271
+ var g = groups[runId];
3272
+ if (!g) {
3273
+ var fileKey = keyOfRun[runId];
3274
+ g = groups[runId] = {
3275
+ key: fileKey,
3276
+ runKey: runId,
3277
+ // provisional; renumbered newest-first below
3278
+ name: ref ? ref.name : fileKey,
3279
+ path: ref ? ref.path : void 0,
3280
+ mime: ref ? ref.mime : void 0,
3281
+ size: ref ? ref.size : void 0,
3282
+ isReindex: !!(ref && ref.isReindex),
3283
+ members: [],
3284
+ passCount: 0,
3285
+ status: "done",
3286
+ cancellableIds: [],
3287
+ cancelling: false,
3288
+ mayHaveOlder: false,
3289
+ anchorIndex: i
3290
+ };
3291
+ order.push(runId);
3292
+ }
3293
+ if (ref) {
3294
+ if (ref.name) g.name = ref.name;
3295
+ if (ref.path) g.path = ref.path;
3296
+ if (ref.mime) g.mime = ref.mime;
3297
+ if (typeof ref.size === "number") g.size = ref.size;
3298
+ if (ref.isReindex) g.isReindex = true;
3299
+ if (!ref.continued) g.mayHaveOlder = false;
3300
+ g.passCount++;
3301
+ }
3302
+ g.members.push({ msg, index: i });
3303
+ g.anchorIndex = i;
3304
+ runOfIndex[i] = runId;
3305
+ if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3306
+ if (ref && ref.name) keyByName[ref.name] = g.key;
3307
+ }
3308
+ for (var rk in runsOfKey) {
3309
+ var runIds = runsOfKey[rk];
3310
+ for (var ri = 0; ri < runIds.length; ri++) {
3311
+ var grpR = groups[runIds[ri]];
3312
+ if (!grpR) continue;
3313
+ var first = grpR.members[0];
3314
+ var firstId = first && first.msg && (first.msg._serverItemId || first.msg._localId);
3315
+ grpR.runKey = rk + "#" + (firstId || "n" + ri);
3316
+ }
3317
+ }
3318
+ for (var oi = 0; oi < order.length; oi++) {
3319
+ var grp = groups[order[oi]];
3320
+ var lastSettled = -1;
3321
+ for (var si = 0; si < grp.members.length; si++) {
3322
+ if (!isPendingMsg(grp.members[si].msg)) lastSettled = si;
3323
+ }
3324
+ var active = false;
3325
+ for (var mi = lastSettled + 1; mi < grp.members.length; mi++) {
3326
+ if (isPendingMsg(grp.members[mi].msg)) {
3327
+ active = true;
3328
+ break;
3329
+ }
3330
+ }
3331
+ for (var xi = 0; xi < grp.members.length; xi++) {
3332
+ if (grp.members[xi].msg._cancelling) {
3333
+ grp.cancelling = true;
3334
+ break;
3335
+ }
3336
+ }
3337
+ var seenIds = {};
3338
+ for (var ci = 0; ci < grp.members.length; ci++) {
3339
+ var cm = grp.members[ci].msg;
3340
+ if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
3341
+ if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
3342
+ if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
3343
+ if (ci < lastSettled) continue;
3344
+ if (seenIds[cm._serverItemId]) continue;
3345
+ seenIds[cm._serverItemId] = true;
3346
+ grp.cancellableIds.push(cm._serverItemId);
3347
+ }
3348
+ if (active) {
3349
+ grp.status = "active";
3350
+ } else {
3351
+ var last = grp.members[grp.members.length - 1].msg;
3352
+ grp.status = last.isError ? "error" : last.isCancelled ? "cancelled" : "done";
3353
+ }
3354
+ var sawFirstPass = false;
3355
+ for (var pi = 0; pi < grp.members.length; pi++) {
3356
+ var pm = grp.members[pi].msg;
3357
+ if (pm.role !== "user") continue;
3358
+ var pref = readFileRef(pm);
3359
+ if (pref && !pref.continued) {
3360
+ sawFirstPass = true;
3361
+ break;
3362
+ }
3363
+ }
3364
+ grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3365
+ }
3366
+ var out = [];
3367
+ for (var j = 0; j < list.length; j++) {
3368
+ var r = runOfIndex[j];
3369
+ if (r === void 0) {
3370
+ out.push({ kind: "message", msg: list[j], index: j });
3371
+ continue;
3372
+ }
3373
+ if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
3374
+ }
3375
+ return out;
3376
+ }
3377
+
3378
+ export { BG_INDEXING_QUEUE_SUFFIX, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, ChatSession, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, OUTPUT_TOKEN_RESERVE, POLL_INTERVAL, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, createHistoryFiller, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAttachmentContent, parseIndexingLabel, readExpiredAttachmentHref, registerAttachmentParser, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
2707
3379
  //# sourceMappingURL=engine.mjs.map
2708
3380
  //# sourceMappingURL=engine.mjs.map