bunnyquery 1.6.0 → 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.cjs CHANGED
@@ -61,6 +61,9 @@ function chatEngineConfig() {
61
61
  }
62
62
  return _config;
63
63
  }
64
+ function windowedIndexingEnabled() {
65
+ return _config?.windowedIndexing === true;
66
+ }
64
67
  function pollOpt() {
65
68
  const p = _config?.poll;
66
69
  return p === void 0 ? {} : { poll: p };
@@ -155,7 +158,32 @@ function isServerExtractable(name, mime) {
155
158
  return m.includes("officedocument") || m.includes("opendocument") || m.includes("hwp") || m.includes("epub") || m === "application/msword" || m === "application/vnd.ms-excel" || m === "application/vnd.ms-powerpoint";
156
159
  }
157
160
  var isOfficeFile = isServerExtractable;
158
- var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set(["xls", "xlsx", "xlsm", "ods", "pdf"]);
161
+ var PAGED_READ_EXTENSIONS = /* @__PURE__ */ new Set([
162
+ // grids
163
+ "xls",
164
+ "xlsx",
165
+ "xlsm",
166
+ "ods",
167
+ // delimited text (row-windowed by the layer)
168
+ "csv",
169
+ "tsv",
170
+ "tab",
171
+ // documents
172
+ "pdf",
173
+ "docx",
174
+ "pptx",
175
+ // plain text / data / markup
176
+ "txt",
177
+ "md",
178
+ "markdown",
179
+ "log",
180
+ "json",
181
+ "jsonl",
182
+ "ndjson",
183
+ "xml",
184
+ "yaml",
185
+ "yml"
186
+ ]);
159
187
  function isPagedReadFile(name, mime) {
160
188
  const ext = (name || "").split(".").pop()?.toLowerCase() || "";
161
189
  if (PAGED_READ_EXTENSIONS.has(ext)) return true;
@@ -179,6 +207,16 @@ function makeRenderPlaceholder(seed) {
179
207
  return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
180
208
  }
181
209
  var RENDER_PAGES_PER_WINDOW = 5;
210
+ var _windowPlaceholderSeq = 0;
211
+ function makeWindowPlaceholder(seed) {
212
+ _windowPlaceholderSeq += 1;
213
+ const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
214
+ return `{{SKAPI_WINDOW::${slug}-${_windowPlaceholderSeq}}}`;
215
+ }
216
+ function isWindowedReadFile(name, mime) {
217
+ if (isImageVisionFile(name, mime)) return false;
218
+ return isPagedReadFile(name, mime);
219
+ }
182
220
  function composeUserMessage(text, attachmentUrls) {
183
221
  let composed = text;
184
222
  if (attachmentUrls.length > 0) {
@@ -252,7 +290,7 @@ File attachments: When a user message contains an "Attached files:" section with
252
290
  - 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.
253
291
  - 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.
254
292
  - 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.
255
- 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.
293
+ 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.
256
294
  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.
257
295
  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:
258
296
  \`\`\`filename.csv
@@ -326,32 +364,60 @@ Read this file with the readFileContent tool, using the storage path above - do
326
364
  }
327
365
  return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
328
366
  }
367
+ var RENDER_FROM_TOKEN = "{{RENDER_FROM}}";
368
+ var WINDOW_CURSOR_TOKEN = RENDER_FROM_TOKEN;
329
369
  function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
330
370
  const from = Math.max(0, renderFrom || 0);
371
+ if (from > 0) return buildIndexingRenderContinueTemplate(attachment, placeholder, String(from + 1));
372
+ return `A new file has just been uploaded. Index it now.
373
+
374
+ ` + buildRenderMeta(attachment) + `
375
+ This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
376
+ ` + buildRenderDatafy(placeholder);
377
+ }
378
+ function buildIndexingRenderContinueTemplate(attachment, placeholder, pageLabel = RENDER_FROM_TOKEN) {
331
379
  const src = `src::${attachment.storagePath}`;
332
- const meta = `File metadata:
380
+ return `CONTINUE indexing a PDF whose previous pass did not finish.
381
+
382
+ ` + buildRenderMeta(attachment) + `
383
+ Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${pageLabel}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
384
+ ` + buildRenderDatafy(placeholder);
385
+ }
386
+ function buildRenderMeta(attachment) {
387
+ return `File metadata:
333
388
  - name: ${attachment.name}
334
389
  - storage path: ${attachment.storagePath}
335
390
  ` + (attachment.mime ? `- mime type: ${attachment.mime}
336
391
  ` : "");
337
- const datafy = `
392
+ }
393
+ function buildRenderDatafy(placeholder) {
394
+ return `
338
395
  ${placeholder}
339
396
 
340
397
  LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.
341
398
 
342
- The note next to the images tells you whether MORE pages remain after this window. If MORE remain: save this window's records and STOP - do NOT write INDEXING_COMPLETE; another pass shows the next window automatically. Only when the note says this is the LAST window (you have seen the whole file) AND everything is saved, end your message with the token INDEXING_COMPLETE.`;
343
- if (from === 0) {
344
- return `A new file has just been uploaded. Index it now.
399
+ Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
400
+ }
401
+ function buildIndexingWindowMessage(attachment, placeholder, isContinuation, positionLabel) {
402
+ const src = `src::${attachment.storagePath}`;
403
+ const head = isContinuation ? `CONTINUE indexing a file whose previous pass did not finish.
345
404
 
346
- ` + meta + `
347
- This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
348
- ` + datafy;
349
- }
350
- return `CONTINUE indexing a PDF whose previous pass did not finish.
405
+ ` : `A new file has just been uploaded. Index it now.
406
+
407
+ `;
408
+ const where = isContinuation ? `
409
+ Records for the earlier windows are ALREADY saved (they reference "${src}"). The NEXT window (starting at ${positionLabel || WINDOW_CURSOR_TOKEN}) is embedded below. Do NOT re-save windows that are already saved.
410
+ ` : `
411
+ This file is delivered to you ONE WINDOW at a time, embedded directly in this message. You do NOT need any tool, URL, or web_fetch to read it.
412
+ `;
413
+ return head + buildRenderMeta(attachment) + where + `
414
+ ${placeholder}
351
415
 
352
- ` + meta + `
353
- Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${from + 1}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
354
- ` + datafy;
416
+ 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.
417
+
418
+ 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.
419
+
420
+ 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.`;
355
421
  }
356
422
  function buildIndexingContinueMessage(attachment) {
357
423
  const src = `src::${attachment.storagePath}`;
@@ -461,8 +527,10 @@ function isAuthExpiredError(input) {
461
527
  var EXPIRED_ATTACHMENT_URL_HOST = "_expired_.url";
462
528
  var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
463
529
  var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
530
+ var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
531
+ var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
464
532
  function createInlineLinkRegex() {
465
- return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]+|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]+|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
533
+ return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
466
534
  }
467
535
  function safeDecodeURIComponent(v) {
468
536
  try {
@@ -522,18 +590,150 @@ function isServiceDbAttachmentHref(href, serviceId) {
522
590
  return false;
523
591
  }
524
592
  }
593
+ function readExpiredAttachmentHref(href) {
594
+ if (!href) return null;
595
+ try {
596
+ var parsed = new URL(href);
597
+ if (parsed.hostname !== EXPIRED_ATTACHMENT_URL_HOST) return null;
598
+ return normalizeAttachmentPathCandidate(parsed.pathname || "") || null;
599
+ } catch (e) {
600
+ return null;
601
+ }
602
+ }
525
603
  function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
526
604
  if (!content) return content;
527
605
  if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
528
606
  return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
529
- if (forAssistant && !isServiceDbAttachmentHref(href, serviceId)) return _m;
607
+ if (!isServiceDbAttachmentHref(href, serviceId)) return _m;
530
608
  var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
531
- var labelPath = normalizeAttachmentPathCandidate(label);
532
- var fullPath = remotePath || labelPath;
533
- if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
609
+ var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
610
+ if (!fullPath) return _m;
534
611
  return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
535
612
  });
536
613
  }
614
+ function isHttpUrlLike(target) {
615
+ return /^https?:\/\//i.test((target || "").trim());
616
+ }
617
+ function repairUrlWhitespace(href) {
618
+ if (!href || !/\s/.test(href)) return href;
619
+ var stripped = href.replace(/\s+/g, "");
620
+ if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
621
+ return href.trim().replace(/\s/g, "%20");
622
+ }
623
+ function normalizeTrailingInlineToken(value) {
624
+ if (!value) return value;
625
+ var out = value.replace(/[.,;:!?]+$/, "");
626
+ var trimUnmatched = function(openCh, closeCh) {
627
+ while (out.charAt(out.length - 1) === closeCh) {
628
+ var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
629
+ var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
630
+ if (closeCount > openCount) out = out.slice(0, -1);
631
+ else break;
632
+ }
633
+ };
634
+ trimUnmatched("(", ")");
635
+ trimUnmatched("[", "]");
636
+ trimUnmatched("{", "}");
637
+ out = out.replace(/[`'"*>]+$/, "");
638
+ return out;
639
+ }
640
+ function classifyInlineLink(full, groups, ctx) {
641
+ var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
642
+ var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
643
+ var fresh = function(expiredHref) {
644
+ return ctx.resolveFreshHref ? ctx.resolveFreshHref(expiredHref) : void 0;
645
+ };
646
+ var isDbHost = function(url) {
647
+ return !!dbHostPrefix && url.toLowerCase().indexOf(dbHostPrefix) === 0;
648
+ };
649
+ var asStoredFile = function(remotePath2, label) {
650
+ if (!remotePath2) return null;
651
+ var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
652
+ var cached = fresh(expiredHref);
653
+ return {
654
+ part: {
655
+ type: "link",
656
+ label: truncateLabelForDisplay(label),
657
+ fullLabel: label,
658
+ href: cached || expiredHref,
659
+ expired: !cached,
660
+ expiredHref,
661
+ remotePath: remotePath2
662
+ }
663
+ };
664
+ };
665
+ if (g1) {
666
+ var rawPath = normalizeTrailingInlineToken(g1);
667
+ var tail = full.slice(("src::" + rawPath).length);
668
+ var srcIsUrl = isHttpUrlLike(rawPath);
669
+ if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
670
+ return {
671
+ part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false },
672
+ tail
673
+ };
674
+ }
675
+ var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.serviceId) || normalizeAttachmentPathCandidate(rawPath) : normalizeAttachmentPathCandidate(rawPath));
676
+ var srcBuilt = asStoredFile(srcPath, srcPath);
677
+ return srcBuilt ? { part: srcBuilt.part, tail } : null;
678
+ }
679
+ if (g4 && g5) {
680
+ var dbTarget = /^db:(.+)$/i.exec(g5.trim());
681
+ if (dbTarget) {
682
+ var declared = asStoredFile(normalizeAttachmentPathCandidate(dbTarget[1]), g4);
683
+ if (!declared) return null;
684
+ declared.part.label = truncateLabelForDisplay(g4);
685
+ declared.part.fullLabel = g4;
686
+ return declared;
687
+ }
688
+ if (isHttpUrlLike(g5)) {
689
+ return classifyInlineLink(full, [void 0, g4, repairUrlWhitespace(g5), void 0, void 0, void 0], ctx);
690
+ }
691
+ var trimmedTarget = g5.trim();
692
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedTarget) || trimmedTarget.charAt(0) === "#") {
693
+ return {
694
+ part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: trimmedTarget, expired: false }
695
+ };
696
+ }
697
+ var built = asStoredFile(normalizeAttachmentPathCandidate(g5), g4);
698
+ if (!built) return null;
699
+ built.part.label = truncateLabelForDisplay(g4);
700
+ built.part.fullLabel = g4;
701
+ return built;
702
+ }
703
+ var originalHref = g3 || g6 || "";
704
+ if (!originalHref) return null;
705
+ var urlTail;
706
+ if (!g3 && g6) {
707
+ var trimmedUrl = normalizeTrailingInlineToken(originalHref);
708
+ if (trimmedUrl !== originalHref) urlTail = originalHref.slice(trimmedUrl.length);
709
+ originalHref = trimmedUrl;
710
+ }
711
+ var withTail = function(r) {
712
+ return urlTail ? { part: r.part, tail: urlTail } : r;
713
+ };
714
+ var urlLabel = g2 || originalHref;
715
+ var carried = readExpiredAttachmentHref(originalHref);
716
+ if (carried) {
717
+ var carriedBuilt = asStoredFile(carried, g2 || carried);
718
+ if (carriedBuilt) {
719
+ if (g2) {
720
+ carriedBuilt.part.label = truncateLabelForDisplay(g2);
721
+ carriedBuilt.part.fullLabel = g2;
722
+ }
723
+ return withTail(carriedBuilt);
724
+ }
725
+ }
726
+ if (isServiceDbAttachmentHref(originalHref, ctx.serviceId)) {
727
+ var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.serviceId);
728
+ if (remotePath) {
729
+ var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
730
+ if (dbBuilt) return withTail(dbBuilt);
731
+ }
732
+ }
733
+ return withTail({
734
+ part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
735
+ });
736
+ }
537
737
  function truncateLabelForDisplay(label) {
538
738
  if (!label) return label;
539
739
  if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
@@ -619,24 +819,27 @@ var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
619
819
  var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
620
820
  var MCP_NAME = "BunnyQuery";
621
821
  var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
622
- var DEFAULT_OPENAI_MODEL = "gpt-5.4";
822
+ var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
623
823
  var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
624
824
  var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
625
825
  var getOpenAIImageDetail = (model) => {
626
826
  const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
627
- const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
827
+ const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
628
828
  if (!match) {
629
829
  return DEFAULT_OPENAI_IMAGE_DETAIL;
630
830
  }
631
831
  const major = Number(match[1]);
632
832
  const minor = match[2] === void 0 ? null : Number(match[2]);
633
- if (major > 5) {
634
- return "original";
635
- }
636
- if (major === 5 && minor !== null && minor >= 4) {
637
- return "original";
833
+ const isVariant = !!match[3];
834
+ const supportsOriginal = major > 5 || major === 5 && minor !== null && minor >= 4;
835
+ if (!supportsOriginal) {
836
+ return DEFAULT_OPENAI_IMAGE_DETAIL;
638
837
  }
639
- return DEFAULT_OPENAI_IMAGE_DETAIL;
838
+ return isVariant ? "high" : "original";
839
+ };
840
+ var getRenderImageDetail = (model) => {
841
+ const detail = getOpenAIImageDetail(model);
842
+ return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? "high" : detail;
640
843
  };
641
844
  var IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
642
845
  function transformContentWithImages(content) {
@@ -881,17 +1084,44 @@ async function notifyAgentSaveAttachment(info) {
881
1084
  const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
882
1085
  const renderFrom = Math.max(0, info.renderFrom || 0);
883
1086
  const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
1087
+ const renderDetail = platform === "openai" ? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL) : void 0;
884
1088
  const skapiRender = visionFile && renderPlaceholder ? {
885
1089
  _skapi_render: [
886
- { path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW, placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime }
1090
+ {
1091
+ path: attachment.storagePath,
1092
+ from: renderFrom,
1093
+ count: RENDER_PAGES_PER_WINDOW,
1094
+ placeholder: renderPlaceholder,
1095
+ name: attachment.name,
1096
+ mime: attachment.mime,
1097
+ detail: renderDetail,
1098
+ auto_continue: true,
1099
+ continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder)
1100
+ }
887
1101
  ]
888
1102
  } : {};
889
- const pagedRead = !visionFile && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
890
- const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
1103
+ const windowedRead = !visionFile && !parsedContent && windowedIndexingEnabled() && isWindowedReadFile(attachment.name, attachment.mime);
1104
+ const windowPlaceholder = windowedRead ? makeWindowPlaceholder(attachment.storagePath) : void 0;
1105
+ const skapiWindow = windowedRead && windowPlaceholder ? {
1106
+ _skapi_window: [
1107
+ {
1108
+ path: attachment.storagePath,
1109
+ cursor: null,
1110
+ placeholder: windowPlaceholder,
1111
+ name: attachment.name,
1112
+ mime: attachment.mime,
1113
+ kind: "window",
1114
+ auto_continue: true,
1115
+ continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true)
1116
+ }
1117
+ ]
1118
+ } : {};
1119
+ const pagedRead = !visionFile && !windowedRead && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
1120
+ const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
891
1121
  const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
892
1122
  const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
893
1123
  const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
894
- const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
1124
+ const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
895
1125
  attachment,
896
1126
  parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
897
1127
  );
@@ -920,6 +1150,7 @@ async function notifyAgentSaveAttachment(info) {
920
1150
  max_output_tokens: MAX_TOKENS,
921
1151
  ...skapiExtract,
922
1152
  ...skapiRender,
1153
+ ...skapiWindow,
923
1154
  input: [
924
1155
  { role: "system", content: systemPrompt },
925
1156
  {
@@ -965,6 +1196,7 @@ async function notifyAgentSaveAttachment(info) {
965
1196
  max_tokens: MAX_TOKENS,
966
1197
  ...skapiExtract,
967
1198
  ...skapiRender,
1199
+ ...skapiWindow,
968
1200
  system: [
969
1201
  {
970
1202
  type: "text",
@@ -1061,9 +1293,15 @@ async function listOpenAIModels(service, owner) {
1061
1293
  });
1062
1294
  }
1063
1295
  var BG_INDEXING_QUEUE_SUFFIX = "-bg";
1296
+ function isBgIndexingQueue(queueName) {
1297
+ if (typeof queueName !== "string" || !queueName) return false;
1298
+ const prefix = queueName.split("|")[0];
1299
+ const idx = prefix.lastIndexOf(":");
1300
+ const name = idx === -1 ? prefix : prefix.slice(idx + 1);
1301
+ return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
1302
+ }
1064
1303
  var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
1065
1304
  var MAX_INDEXING_RESUME_PASSES = 6;
1066
- var MAX_VISION_RESUME_PASSES = 40;
1067
1305
  async function getChatHistory(params, fetchOptions) {
1068
1306
  const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
1069
1307
  const p = Object.assign(
@@ -1127,18 +1365,29 @@ function mapHistoryListToMessages(list, platform, opts) {
1127
1365
  var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
1128
1366
  if (userText) {
1129
1367
  var displayContent;
1368
+ var indexFile = void 0;
1130
1369
  if (item._isBgTask) {
1131
1370
  var nameMatch = userText.match(/^- name: (.+)$/m);
1132
1371
  if (nameMatch) {
1133
1372
  var mimeMatch = userText.match(/^- mime type: (.+)$/m);
1134
1373
  var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
1135
1374
  var pathMatch = userText.match(/^- storage path: (.+)$/m);
1375
+ var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
1136
1376
  displayContent = opts.formatIndexingLabel(
1137
1377
  nameMatch[1].trim(),
1138
1378
  mimeMatch ? mimeMatch[1].trim() : "",
1139
1379
  sizeMatch ? Number(sizeMatch[1]) : null,
1140
- pathMatch ? pathMatch[1].trim() : void 0
1380
+ pathMatch ? pathMatch[1].trim() : void 0,
1381
+ false,
1382
+ isContinuePass
1141
1383
  );
1384
+ indexFile = {
1385
+ name: nameMatch[1].trim(),
1386
+ path: pathMatch ? pathMatch[1].trim() : void 0,
1387
+ mime: mimeMatch ? mimeMatch[1].trim() : void 0,
1388
+ size: sizeMatch ? Number(sizeMatch[1]) : void 0,
1389
+ continued: isContinuePass
1390
+ };
1142
1391
  } else {
1143
1392
  displayContent = userText;
1144
1393
  }
@@ -1150,6 +1399,7 @@ function mapHistoryListToMessages(list, platform, opts) {
1150
1399
  if (isQueued) userMsg.isPendingQueued = true;
1151
1400
  if (isCancelledItem) userMsg.isCancelled = true;
1152
1401
  if (item._isBgTask) userMsg.isBackgroundTask = true;
1402
+ if (indexFile) userMsg._indexFile = indexFile;
1153
1403
  if (item._isOnBgQueue) userMsg._useBgQueue = true;
1154
1404
  if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
1155
1405
  mapped.push(userMsg);
@@ -1177,6 +1427,91 @@ function mapHistoryListToMessages(list, platform, opts) {
1177
1427
  return { messages: mapped, runningItemIds };
1178
1428
  }
1179
1429
 
1430
+ // src/engine/viewport_fill.ts
1431
+ var HISTORY_FILL_SLACK_PX = 64;
1432
+ var MAX_HISTORY_FILL_PAGES = 24;
1433
+ var IDLE_WAIT_STEP_MS = 120;
1434
+ var IDLE_WAIT_MAX_MS = 15e3;
1435
+ async function waitForIdle(opts, stale) {
1436
+ var waited = 0;
1437
+ while (opts.isLoading()) {
1438
+ if (stale() || waited >= IDLE_WAIT_MAX_MS) return false;
1439
+ await new Promise(function(r) {
1440
+ setTimeout(r, IDLE_WAIT_STEP_MS);
1441
+ });
1442
+ waited += IDLE_WAIT_STEP_MS;
1443
+ }
1444
+ return !stale();
1445
+ }
1446
+ async function fillHistoryViewport(opts) {
1447
+ var maxPages = typeof opts.maxPages === "number" ? opts.maxPages : MAX_HISTORY_FILL_PAGES;
1448
+ var stale = function() {
1449
+ return !!(opts.isStale && opts.isStale());
1450
+ };
1451
+ var swallowed = 0;
1452
+ for (var page = 0; page < maxPages; page++) {
1453
+ if (stale() || opts.isEndOfList()) return;
1454
+ if (!await waitForIdle(opts, stale)) return;
1455
+ var satisfied = false;
1456
+ try {
1457
+ satisfied = !!await opts.isSatisfied();
1458
+ } catch {
1459
+ return;
1460
+ }
1461
+ if (satisfied || stale()) return;
1462
+ if (!await waitForIdle(opts, stale)) return;
1463
+ var before = opts.messageCount();
1464
+ var attempted;
1465
+ try {
1466
+ attempted = await opts.fetchOlder();
1467
+ } catch {
1468
+ return;
1469
+ }
1470
+ if (stale()) return;
1471
+ if (attempted === false) {
1472
+ if (++swallowed > 3) return;
1473
+ page--;
1474
+ continue;
1475
+ }
1476
+ if (opts.messageCount() <= before) return;
1477
+ }
1478
+ }
1479
+ function createHistoryFiller(base) {
1480
+ var pending = [];
1481
+ var running = false;
1482
+ async function allSatisfied() {
1483
+ var next = [];
1484
+ for (var i = 0; i < pending.length; i++) {
1485
+ if (!await pending[i]()) next.push(pending[i]);
1486
+ }
1487
+ pending = next;
1488
+ return pending.length === 0;
1489
+ }
1490
+ return {
1491
+ isRunning: function() {
1492
+ return running;
1493
+ },
1494
+ fill: function(isSatisfied) {
1495
+ pending.push(isSatisfied);
1496
+ if (running) return Promise.resolve();
1497
+ running = true;
1498
+ var done = function() {
1499
+ running = false;
1500
+ pending = [];
1501
+ };
1502
+ return fillHistoryViewport({
1503
+ isSatisfied: allSatisfied,
1504
+ isEndOfList: base.isEndOfList,
1505
+ isLoading: base.isLoading,
1506
+ messageCount: base.messageCount,
1507
+ fetchOlder: base.fetchOlder,
1508
+ isStale: base.isStale,
1509
+ maxPages: base.maxPages
1510
+ }).then(done, done);
1511
+ }
1512
+ };
1513
+ }
1514
+
1180
1515
  // src/engine/session.ts
1181
1516
  var _g = typeof globalThis !== "undefined" ? globalThis : {};
1182
1517
  function nowMs() {
@@ -1191,6 +1526,9 @@ function nextFrame(cb) {
1191
1526
  cb(nowMs());
1192
1527
  }, 16);
1193
1528
  }
1529
+ function isPollStopped(res) {
1530
+ return !!res && typeof res === "object" && res.status === "stopped";
1531
+ }
1194
1532
  var ChatSession = class {
1195
1533
  constructor(host) {
1196
1534
  this.typewriterQueue = Promise.resolve();
@@ -1211,11 +1549,104 @@ var ChatSession = class {
1211
1549
  };
1212
1550
  this.bgTaskQueue = [];
1213
1551
  this.cancelledServerIds = /* @__PURE__ */ new Set();
1552
+ this.cancelledIndexKeys = /* @__PURE__ */ new Set();
1214
1553
  this.pendingAgentRequests = {};
1215
1554
  this.aiChatHistoryCache = {};
1216
1555
  this.historyItemPolls = /* @__PURE__ */ new Map();
1556
+ this._pauseReasons = /* @__PURE__ */ new Set();
1557
+ this._resuming = false;
1217
1558
  this._lidSeq = 0;
1218
1559
  }
1560
+ /**
1561
+ * Register a live poll so (a) a remount dedupes against it instead of stacking a
1562
+ * SECOND poll on the same item, and (b) pausePolling can stop it.
1563
+ *
1564
+ * `stop` comes from the SDK and may be absent on an older skapi-js, in which case the
1565
+ * poll simply cannot be stopped and is left running — see pausePolling.
1566
+ */
1567
+ _trackPoll(id, kind, p) {
1568
+ var stop = p && typeof p.stop === "function" ? p.stop.bind(p) : void 0;
1569
+ if (!stop) {
1570
+ console.debug("[chat-engine] poll has no stop handle", { id, kind });
1571
+ }
1572
+ this.historyItemPolls.set(id, { kind, stop });
1573
+ return p;
1574
+ }
1575
+ /**
1576
+ * Stop and forget one item's poll. Used after a cancel: the row is either gone
1577
+ * (cancelled while queued) or flagged cancelled (cancelled while running), so
1578
+ * asking about it again only burns requests. Safe when no poll is attached, and
1579
+ * safe on an older skapi-js with no stop handle (the entry is then LEFT in the
1580
+ * map so a later drain cannot stack a second, unstoppable poll on the id).
1581
+ */
1582
+ _stopPoll(id) {
1583
+ var handle = this.historyItemPolls.get(id);
1584
+ if (!handle) return;
1585
+ if (typeof handle.stop !== "function") return;
1586
+ try {
1587
+ handle.stop();
1588
+ } catch (e) {
1589
+ }
1590
+ this.historyItemPolls.delete(id);
1591
+ }
1592
+ /** True while any pause reason is active. */
1593
+ isPollingPaused() {
1594
+ return this._pauseReasons.size > 0;
1595
+ }
1596
+ /**
1597
+ * Stop BACKGROUND polling until resumePolling. Foreground polls (a reply the user is
1598
+ * waiting on) keep running deliberately: their results must still land in the history
1599
+ * cache so resumePendingRequest can render them on return, otherwise a user who sends
1600
+ * a message then navigates away comes back to a permanently stuck "Thinking...".
1601
+ *
1602
+ * Server-side work is untouched; this only stops asking about it. That is safe for
1603
+ * document indexing because the worker drives that loop itself.
1604
+ */
1605
+ pausePolling(reason) {
1606
+ this._pauseReasons.add(reason || "paused");
1607
+ var self = this;
1608
+ var stopped = [];
1609
+ this.historyItemPolls.forEach(function(handle, id) {
1610
+ if (!handle || handle.kind !== "bg") return;
1611
+ if (typeof handle.stop !== "function") return;
1612
+ try {
1613
+ handle.stop();
1614
+ } catch (e) {
1615
+ }
1616
+ stopped.push(id);
1617
+ });
1618
+ stopped.forEach(function(id) {
1619
+ self.historyItemPolls.delete(id);
1620
+ });
1621
+ }
1622
+ /**
1623
+ * Lift a pause reason WITHOUT running the reconcile. For a caller that is about to
1624
+ * reload history anyway (a view remounting), letting resumePolling also reconcile
1625
+ * would race that load and can double-attach.
1626
+ */
1627
+ clearPauseReason(reason) {
1628
+ this._pauseReasons.delete(reason || "paused");
1629
+ }
1630
+ /**
1631
+ * Clear a pause reason and, once none remain, re-attach polling and reconcile.
1632
+ * Deliberately does NOT touch gateRefreshToken: bumping it would silently discard
1633
+ * the results of anything still in flight across the pause.
1634
+ */
1635
+ resumePolling(reason) {
1636
+ this._pauseReasons.delete(reason || "paused");
1637
+ if (this._pauseReasons.size > 0 || this._resuming) return Promise.resolve();
1638
+ if (!this.host.isViewMounted || !this.host.isViewMounted()) return Promise.resolve();
1639
+ var self = this;
1640
+ this._resuming = true;
1641
+ return Promise.resolve().then(function() {
1642
+ self.drainBgTaskQueue();
1643
+ return self.loadHistory(false, self.state.gateRefreshToken);
1644
+ }).catch(function(e) {
1645
+ console.error("[chat-engine] resume polling failed", e);
1646
+ }).then(function() {
1647
+ self._resuming = false;
1648
+ });
1649
+ }
1219
1650
  _newLocalId() {
1220
1651
  this._lidSeq += 1;
1221
1652
  return "lid_" + this._lidSeq;
@@ -1229,29 +1660,97 @@ var ChatSession = class {
1229
1660
  var key = this.getHistoryCacheKey();
1230
1661
  if (!key) return;
1231
1662
  this.aiChatHistoryCache[key] = {
1232
- messages: this.state.messages.slice(),
1663
+ messages: this.state.messages.filter(function(m) {
1664
+ return m._ownerKey === void 0 || m._ownerKey === key;
1665
+ }),
1233
1666
  endOfList: this.state.historyEndOfList,
1234
1667
  startKeyHistory: this.state.historyStartKeyHistory.slice()
1235
1668
  };
1236
1669
  }
1237
- _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls) {
1238
- var id = this.host.getIdentity();
1239
- return platform === "openai" ? callOpenAIWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, id.serviceId, id.owner, messages, system, model, userId, extractContent, fileUrls);
1670
+ /**
1671
+ * Land a resolved reply in the history cache of a chat that is NOT currently
1672
+ * visible, without touching state.messages. Mirrors the cache-only path in
1673
+ * dispatchAgentRequest: REPLACE the trailing pending "Thinking..." bubble
1674
+ * (append only when there is none), and settle the matching pending user
1675
+ * bubble, so the cached copy never keeps a stuck "Thinking..." that a later
1676
+ * cache-first load would re-render forever.
1677
+ */
1678
+ _applyReplyToCache(key, reply, serverId) {
1679
+ if (!key) return;
1680
+ var existing = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1681
+ var msgs = existing.messages.slice();
1682
+ var thIdx = -1;
1683
+ for (var i = msgs.length - 1; i >= 0; i--) {
1684
+ var m = msgs[i];
1685
+ if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
1686
+ if (serverId && m._serverItemId && m._serverItemId !== serverId) continue;
1687
+ thIdx = i;
1688
+ break;
1689
+ }
1690
+ if (thIdx !== -1) {
1691
+ if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
1692
+ msgs[thIdx] = reply;
1693
+ } else {
1694
+ var dupIdx = -1;
1695
+ if (serverId) {
1696
+ for (var d = msgs.length - 1; d >= 0; d--) {
1697
+ var dm = msgs[d];
1698
+ if (dm && dm.role === "assistant" && dm._serverItemId === serverId) {
1699
+ dupIdx = d;
1700
+ break;
1701
+ }
1702
+ }
1703
+ }
1704
+ if (dupIdx !== -1) msgs[dupIdx] = reply;
1705
+ else msgs.push(reply);
1706
+ }
1707
+ for (var j = 0; j < msgs.length; j++) {
1708
+ var u = msgs[j];
1709
+ if (!u || u.role !== "user" || u.isBackgroundTask) continue;
1710
+ if (!(u.isPendingQueued || u.isPendingInProcess || u.isSendingToServer)) continue;
1711
+ if (serverId && u._serverItemId && u._serverItemId !== serverId) continue;
1712
+ var settled = { role: "user", content: u.content };
1713
+ if (u._serverItemId !== void 0) settled._serverItemId = u._serverItemId;
1714
+ if (u._ownerKey !== void 0) settled._ownerKey = u._ownerKey;
1715
+ msgs[j] = settled;
1716
+ break;
1717
+ }
1718
+ this.aiChatHistoryCache[key] = {
1719
+ messages: msgs,
1720
+ endOfList: existing.endOfList,
1721
+ startKeyHistory: existing.startKeyHistory
1722
+ };
1723
+ }
1724
+ /**
1725
+ * serviceId/owner are passed explicitly by every caller: a request can be
1726
+ * dispatched after the user moved to another project, and re-reading the live
1727
+ * identity here would silently send the turn to THAT project instead of the
1728
+ * one it was composed for. Falls back to the live read only when a caller
1729
+ * omits them.
1730
+ */
1731
+ _callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls, serviceId, owner) {
1732
+ if (serviceId === void 0 || owner === void 0) {
1733
+ var id = this.host.getIdentity();
1734
+ if (serviceId === void 0) serviceId = id.serviceId;
1735
+ if (owner === void 0) owner = id.owner;
1736
+ }
1737
+ return platform === "openai" ? callOpenAIWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, serviceId, owner, messages, system, model, userId, extractContent, fileUrls);
1240
1738
  }
1241
1739
  dispatchAgentRequest(params) {
1242
1740
  var self = this;
1243
1741
  var dispatchItemId;
1244
1742
  var sendAndPoll = function() {
1245
1743
  return Promise.resolve(
1246
- self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls)
1744
+ self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.serviceId, params.owner)
1247
1745
  ).then(function(initial) {
1248
1746
  if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
1249
1747
  if (initial.id) {
1250
1748
  if (dispatchItemId && dispatchItemId !== initial.id) self.historyItemPolls.delete(dispatchItemId);
1251
1749
  dispatchItemId = initial.id;
1252
- self.historyItemPolls.set(initial.id, true);
1253
1750
  }
1254
- return initial.poll({ latency: POLL_INTERVAL });
1751
+ var dp = initial.poll({ latency: POLL_INTERVAL });
1752
+ if (initial.id) self._trackPoll(initial.id, "fg", dp);
1753
+ return dp;
1255
1754
  }
1256
1755
  return initial;
1257
1756
  });
@@ -1304,20 +1803,57 @@ var ChatSession = class {
1304
1803
  // composed = clean display text; composedForLlm carries office-extraction
1305
1804
  // placeholders for the provider only. useBgQueue routes a post-attachment turn
1306
1805
  // onto the "-bg" queue so it runs after indexing.
1307
- dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls) {
1806
+ dispatchComposedMessage(composed, useBgQueue, composedForLlm, extractContent, fileUrls, pinned) {
1308
1807
  var self = this;
1309
1808
  if (!composed) return;
1310
- var id = this.host.getIdentity();
1809
+ var id = pinned ? pinned.identity : this.host.getIdentity();
1311
1810
  if (id.platform === "none") return;
1312
1811
  var llmComposed = composedForLlm || composed;
1313
- var isQueuedSend = useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1812
+ var key = !id.serviceId ? "" : id.serviceId + "#" + id.platform;
1813
+ var offChat = !!key && key !== this.getHistoryCacheKey();
1814
+ var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
1314
1815
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
1315
- });
1816
+ }));
1316
1817
  var aiPlatform = id.platform;
1317
1818
  var aiModel = id.model || void 0;
1318
- var systemPrompt = this.host.buildSystemPrompt();
1819
+ var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
1319
1820
  var userId = id.userId || id.serviceId;
1320
1821
  var chatQueue = useBgQueue ? userId + BG_INDEXING_QUEUE_SUFFIX : userId;
1822
+ if (offChat) {
1823
+ var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
1824
+ return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
1825
+ });
1826
+ var offBounded = buildBoundedChatMessages({
1827
+ platform: aiPlatform,
1828
+ model: aiModel,
1829
+ systemPrompt,
1830
+ serviceId: id.serviceId,
1831
+ history: offHistory.concat([{ role: "user", content: llmComposed }])
1832
+ });
1833
+ var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
1834
+ this.aiChatHistoryCache[key] = {
1835
+ messages: offExisting.messages.concat([
1836
+ { role: "user", content: composed, _ownerKey: key },
1837
+ { role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
1838
+ ]),
1839
+ endOfList: offExisting.endOfList,
1840
+ startKeyHistory: offExisting.startKeyHistory
1841
+ };
1842
+ this.dispatchAgentRequest({
1843
+ key,
1844
+ serviceId: id.serviceId,
1845
+ owner: id.owner,
1846
+ aiPlatform,
1847
+ aiModel,
1848
+ systemPrompt,
1849
+ text: composed,
1850
+ boundedMessages: offBounded.messages,
1851
+ userId: chatQueue,
1852
+ extractContent,
1853
+ fileUrls
1854
+ });
1855
+ return;
1856
+ }
1321
1857
  if (isQueuedSend) {
1322
1858
  var resolvedHistory = this.state.messages.filter(function(m) {
1323
1859
  return !m.isPending && !m.isPendingQueued && !m.isPendingInProcess && !m.isPendingOlder && !m.isCancelled && !m.isBackgroundTask;
@@ -1330,15 +1866,16 @@ var ChatSession = class {
1330
1866
  history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
1331
1867
  });
1332
1868
  var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
1869
+ if (key) queuedBubble._ownerKey = key;
1333
1870
  if (useBgQueue) queuedBubble._useBgQueue = true;
1334
1871
  this.state.messages.push(queuedBubble);
1335
1872
  this.host.notify();
1336
1873
  this.updateHistoryCache();
1337
1874
  this.host.scrollToBottom(true);
1338
- var capturedComposed = composed, capturedPlatform = aiPlatform;
1339
- Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls)).then(function(result) {
1340
- var sendingIdx = self.state.messages.findIndex(function(m) {
1341
- return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1875
+ var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
1876
+ Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.serviceId, id.owner)).then(function(result) {
1877
+ var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
1878
+ return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
1342
1879
  });
1343
1880
  var serverId = result && typeof result.id === "string" ? result.id : void 0;
1344
1881
  if (sendingIdx >= 0) {
@@ -1348,26 +1885,27 @@ var ChatSession = class {
1348
1885
  self.host.notify();
1349
1886
  }
1350
1887
  if (result && result.poll && (result.status === "pending" || result.status === "running")) {
1351
- if (serverId) self.historyItemPolls.set(serverId, true);
1352
- return result.poll({ latency: POLL_INTERVAL }).then(function(res) {
1353
- return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId);
1888
+ var qp = result.poll({ latency: POLL_INTERVAL });
1889
+ if (serverId) self._trackPoll(serverId, "fg", qp);
1890
+ return qp.then(function(res) {
1891
+ if (isPollStopped(res)) return;
1892
+ return self.onQueuedSendResponse(capturedComposed, res, capturedPlatform, serverId, capturedKey);
1354
1893
  }).catch(function(err) {
1355
- return self.onQueuedSendError(capturedComposed, err, serverId);
1894
+ return self.onQueuedSendError(capturedComposed, err, serverId, capturedKey);
1356
1895
  });
1357
1896
  }
1358
- return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId);
1897
+ return self.onQueuedSendResponse(capturedComposed, result, capturedPlatform, serverId, capturedKey);
1359
1898
  }).catch(function(err) {
1360
- return self.onQueuedSendError(capturedComposed, err, void 0);
1899
+ return self.onQueuedSendError(capturedComposed, err, void 0, capturedKey);
1361
1900
  });
1362
1901
  return;
1363
1902
  }
1364
- this.state.messages.push({ role: "user", content: composed });
1365
- this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true });
1903
+ this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
1904
+ this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
1366
1905
  this.host.notify();
1367
1906
  this.updateHistoryCache();
1368
1907
  this.state.sending = true;
1369
1908
  this.host.scrollToBottom(true);
1370
- var key = this.getHistoryCacheKey();
1371
1909
  var historyForLlm = this.state.messages.filter(function(m) {
1372
1910
  return !m.isCancelled && !m.isBackgroundTask;
1373
1911
  });
@@ -1401,10 +1939,10 @@ var ChatSession = class {
1401
1939
  });
1402
1940
  Promise.resolve(run).catch(function() {
1403
1941
  }).then(function() {
1404
- if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1405
1942
  self.state.sending = false;
1943
+ if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
1406
1944
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1407
- self.host.scrollToBottom(true);
1945
+ self.host.scrollToBottomIfSticky(true);
1408
1946
  });
1409
1947
  });
1410
1948
  }
@@ -1418,10 +1956,13 @@ var ChatSession = class {
1418
1956
  if (nextIdx === -1) return;
1419
1957
  var existing = this.state.messages[nextIdx];
1420
1958
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
1959
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
1421
1960
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1961
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1422
1962
  this.state.messages[nextIdx] = promoted;
1423
1963
  var placeholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true };
1424
1964
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1965
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1425
1966
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1426
1967
  this.host.notify();
1427
1968
  }
@@ -1436,15 +1977,22 @@ var ChatSession = class {
1436
1977
  var existing = this.state.messages[nextIdx];
1437
1978
  var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
1438
1979
  if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
1980
+ if (existing._indexFile) promoted._indexFile = existing._indexFile;
1439
1981
  if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
1982
+ if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
1440
1983
  if (existing.isSendingToServer) promoted.isSendingToServer = true;
1441
1984
  this.state.messages[nextIdx] = promoted;
1442
1985
  var placeholder = { role: "assistant", content: "", isPending: true };
1443
1986
  if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
1987
+ if (existing._ownerKey !== void 0) placeholder._ownerKey = existing._ownerKey;
1444
1988
  this.state.messages.splice(nextIdx + 1, 0, placeholder);
1445
1989
  this.host.notify();
1446
1990
  }
1447
1991
  resolveQueuedUserBubble(serverId) {
1992
+ var liveKey = this.getHistoryCacheKey();
1993
+ var isLocal = function(m) {
1994
+ return m._ownerKey === void 0 || m._ownerKey === liveKey;
1995
+ };
1448
1996
  var userIdx = -1;
1449
1997
  if (serverId) {
1450
1998
  userIdx = this.state.messages.findIndex(function(m) {
@@ -1453,19 +2001,19 @@ var ChatSession = class {
1453
2001
  }
1454
2002
  if (userIdx === -1) {
1455
2003
  userIdx = this.state.messages.findIndex(function(m) {
1456
- return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
2004
+ return m.isPendingInProcess && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1457
2005
  });
1458
2006
  }
1459
2007
  if (userIdx === -1) {
1460
2008
  userIdx = this.state.messages.findIndex(function(m) {
1461
- return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue;
2009
+ return m.isPendingQueued && m.role === "user" && !m.isBackgroundTask && !m._useBgQueue && isLocal(m);
1462
2010
  });
1463
2011
  }
1464
2012
  if (serverId && this.cancelledServerIds.has(serverId)) {
1465
2013
  this.cancelledServerIds.delete(serverId);
1466
2014
  if (userIdx >= 0) {
1467
2015
  var ex = this.state.messages[userIdx];
1468
- this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
2016
+ this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId, ...ex._ownerKey !== void 0 ? { _ownerKey: ex._ownerKey } : {} };
1469
2017
  var thIdx = this.state.messages.findIndex(function(m, i) {
1470
2018
  return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
1471
2019
  });
@@ -1478,6 +2026,7 @@ var ChatSession = class {
1478
2026
  var exist = this.state.messages[userIdx];
1479
2027
  var repl = { role: "user", content: exist.content };
1480
2028
  if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
2029
+ if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
1481
2030
  this.state.messages[userIdx] = repl;
1482
2031
  }
1483
2032
  var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
@@ -1490,8 +2039,14 @@ var ChatSession = class {
1490
2039
  else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
1491
2040
  else this.state.messages.push(msg);
1492
2041
  }
1493
- onQueuedSendResponse(_composed, response, platform, serverId) {
2042
+ onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
1494
2043
  if (serverId) this.historyItemPolls.delete(serverId);
2044
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
2045
+ var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
2046
+ this._applyReplyToCache(ownerKey, offReply, serverId);
2047
+ if (serverId) this.cancelledServerIds.delete(serverId);
2048
+ return;
2049
+ }
1495
2050
  var targetIdx = this.resolveQueuedUserBubble(serverId);
1496
2051
  if (targetIdx === void 0) {
1497
2052
  this.host.notify();
@@ -1523,10 +2078,16 @@ var ChatSession = class {
1523
2078
  this.promoteNextQueuedToRunning();
1524
2079
  this.updateHistoryCache();
1525
2080
  this.host.notify();
1526
- this.host.scrollToBottom(true);
2081
+ this.host.scrollToBottomIfSticky(true);
1527
2082
  }
1528
- onQueuedSendError(_composed, err, serverId) {
2083
+ onQueuedSendError(_composed, err, serverId, ownerKey) {
1529
2084
  if (serverId) this.historyItemPolls.delete(serverId);
2085
+ if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
2086
+ var isGone = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
2087
+ this._applyReplyToCache(ownerKey, isGone ? { role: "assistant", content: "Request was cancelled.", isError: true } : { role: "assistant", content: getErrorMessage(err), isError: true }, serverId);
2088
+ if (serverId) this.cancelledServerIds.delete(serverId);
2089
+ return;
2090
+ }
1530
2091
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
1531
2092
  if (isNotExists) {
1532
2093
  var userIdx = serverId ? this.state.messages.findIndex(function(m) {
@@ -1567,7 +2128,7 @@ var ChatSession = class {
1567
2128
  this.promoteNextQueuedToRunning();
1568
2129
  this.updateHistoryCache();
1569
2130
  this.host.notify();
1570
- this.host.scrollToBottom(true);
2131
+ this.host.scrollToBottomIfSticky(true);
1571
2132
  return;
1572
2133
  }
1573
2134
  var targetIdx = this.resolveQueuedUserBubble(serverId);
@@ -1581,7 +2142,7 @@ var ChatSession = class {
1581
2142
  this.promoteNextQueuedToRunning();
1582
2143
  this.updateHistoryCache();
1583
2144
  this.host.notify();
1584
- this.host.scrollToBottom(true);
2145
+ this.host.scrollToBottomIfSticky(true);
1585
2146
  }
1586
2147
  cancelQueuedMessage(msg, idx) {
1587
2148
  var self = this;
@@ -1605,6 +2166,7 @@ var ChatSession = class {
1605
2166
  })).then(function(result) {
1606
2167
  if (result && result.removed) {
1607
2168
  self.cancelledServerIds.add(serverId);
2169
+ self._stopPoll(serverId);
1608
2170
  var qi = self.bgTaskQueue.findIndex(function(e) {
1609
2171
  return e.id === serverId;
1610
2172
  });
@@ -1613,7 +2175,13 @@ var ChatSession = class {
1613
2175
  return m._serverItemId === serverId && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
1614
2176
  });
1615
2177
  if (removeIdx !== -1) {
1616
- self.state.messages[removeIdx] = { role: "user", content: self.state.messages[removeIdx].content, isCancelled: true, _serverItemId: serverId };
2178
+ var wasMsg = self.state.messages[removeIdx];
2179
+ var cancelledMsg = { role: "user", content: wasMsg.content, isCancelled: true, _serverItemId: serverId };
2180
+ if (wasMsg.isBackgroundTask) cancelledMsg.isBackgroundTask = true;
2181
+ if (wasMsg._indexFile) cancelledMsg._indexFile = wasMsg._indexFile;
2182
+ if (wasMsg._useBgQueue) cancelledMsg._useBgQueue = true;
2183
+ if (wasMsg._ownerKey !== void 0) cancelledMsg._ownerKey = wasMsg._ownerKey;
2184
+ self.state.messages[removeIdx] = cancelledMsg;
1617
2185
  var thById = self.state.messages.findIndex(function(m) {
1618
2186
  return m._serverItemId === serverId && m.isPending && m.role === "assistant";
1619
2187
  });
@@ -1650,6 +2218,42 @@ var ChatSession = class {
1650
2218
  }
1651
2219
  });
1652
2220
  }
2221
+ /**
2222
+ * Stop indexing a file, from its collapsed row — every pass at once, not just
2223
+ * the bubble the user happens to see.
2224
+ *
2225
+ * A big file is indexed as a CHAIN of passes, so cancelling only the live one
2226
+ * accomplishes nothing: the next pass is dispatched as soon as it settles.
2227
+ * Three things end the chain:
2228
+ * 1. every queued/running pass of this file is cancelled server-side
2229
+ * (csr-cancel deletes a queued row and flags a running one "cancelled",
2230
+ * which is also the worker's gate for NOT enqueueing the next window);
2231
+ * 2. the file is remembered in cancelledIndexKeys, so the client-driven
2232
+ * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
2233
+ * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
2234
+ * drain rather than surfacing a fresh "Indexing…" bubble.
2235
+ *
2236
+ * Records already written by the passes that DID run are kept — this stops the
2237
+ * work, it does not undo it.
2238
+ */
2239
+ cancelIndexingGroup(group) {
2240
+ var self = this;
2241
+ if (!group || !group.key) return;
2242
+ var scoped = this.getHistoryCacheKey() + "|" + group.key;
2243
+ this.cancelledIndexKeys.add(scoped);
2244
+ var ids = group.cancellableIds || [];
2245
+ if (!ids.length) {
2246
+ this.host.notify();
2247
+ return;
2248
+ }
2249
+ ids.forEach(function(serverId) {
2250
+ var idx = self.state.messages.findIndex(function(m) {
2251
+ return m._serverItemId === serverId && m.role === "user" && (m.isPendingQueued || m.isPendingInProcess);
2252
+ });
2253
+ if (idx === -1) return;
2254
+ self.cancelQueuedMessage(self.state.messages[idx], idx);
2255
+ });
2256
+ }
1653
2257
  // --- typewriter -------------------------------------------------------
1654
2258
  // Reveal `fullText` into a message bubble at a constant wall-clock RATE
1655
2259
  // (chars/second) driven by requestAnimationFrame, rather than a fixed number
@@ -1836,6 +2440,7 @@ var ChatSession = class {
1836
2440
  var u = this.state.messages[uIdx];
1837
2441
  var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
1838
2442
  if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
2443
+ if (u._indexFile) cleaned._indexFile = u._indexFile;
1839
2444
  this.state.messages[uIdx] = cleaned;
1840
2445
  }
1841
2446
  // If an immediate-send request for the current cache key is still in flight
@@ -1853,13 +2458,13 @@ var ChatSession = class {
1853
2458
  return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
1854
2459
  })) return Promise.resolve();
1855
2460
  this.state.sending = true;
1856
- this.host.scrollToBottom(true);
2461
+ this.host.scrollToBottomIfSticky(true);
1857
2462
  return Promise.resolve(pending).catch(function() {
1858
2463
  }).then(function() {
1859
2464
  if (token !== self.state.gateRefreshToken) return;
1860
2465
  self.state.sending = false;
1861
2466
  return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
1862
- self.host.scrollToBottom(true);
2467
+ self.host.scrollToBottomIfSticky(true);
1863
2468
  });
1864
2469
  });
1865
2470
  }
@@ -1878,8 +2483,17 @@ var ChatSession = class {
1878
2483
  });
1879
2484
  if (idx !== -1) {
1880
2485
  this._clearPendingUserBubble(itemId);
2486
+ var wasBgTask = !!this.state.messages[idx].isBackgroundTask;
1881
2487
  if (isErr) {
1882
2488
  this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2489
+ if (wasBgTask) this.state.messages[idx].isBackgroundTask = true;
2490
+ this.host.notify();
2491
+ this.updateHistoryCache();
2492
+ return;
2493
+ }
2494
+ var text = answer || "No text response received from AI provider.";
2495
+ if (wasBgTask) {
2496
+ this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
1883
2497
  this.host.notify();
1884
2498
  this.updateHistoryCache();
1885
2499
  return;
@@ -1887,7 +2501,7 @@ var ChatSession = class {
1887
2501
  var lid = this._newLocalId();
1888
2502
  this.state.messages[idx] = { role: "assistant", content: "", _localId: lid, _serverItemId: itemId };
1889
2503
  this.host.notify();
1890
- this.enqueueTypewrite(idx, answer || "No text response received from AI provider.", lid);
2504
+ this.enqueueTypewrite(idx, text, lid);
1891
2505
  this.updateHistoryCache();
1892
2506
  return;
1893
2507
  }
@@ -1896,25 +2510,124 @@ var ChatSession = class {
1896
2510
  });
1897
2511
  if (userIdx === -1) return;
1898
2512
  var ex = this.state.messages[userIdx];
1899
- this.state.messages[userIdx] = { role: "user", content: ex.content, _serverItemId: itemId };
2513
+ var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
2514
+ if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
2515
+ if (ex._indexFile) settledUser._indexFile = ex._indexFile;
2516
+ if (ex._useBgQueue) settledUser._useBgQueue = true;
2517
+ this.state.messages[userIdx] = settledUser;
1900
2518
  if (isErr) {
1901
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: answer, isError: true, _serverItemId: itemId });
2519
+ var errReply = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
2520
+ if (ex.isBackgroundTask) errReply.isBackgroundTask = true;
2521
+ this.state.messages.splice(userIdx + 1, 0, errReply);
2522
+ this.host.notify();
2523
+ this.updateHistoryCache();
2524
+ return;
2525
+ }
2526
+ var text2 = answer || "No text response received from AI provider.";
2527
+ if (ex.isBackgroundTask) {
2528
+ this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
1902
2529
  this.host.notify();
1903
2530
  this.updateHistoryCache();
1904
2531
  return;
1905
2532
  }
1906
2533
  var lid2 = this._newLocalId();
1907
- this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId });
2534
+ var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
2535
+ this.state.messages.splice(userIdx + 1, 0, reply);
1908
2536
  this.host.notify();
1909
- this.enqueueTypewrite(userIdx + 1, answer || "No text response received from AI provider.", lid2);
2537
+ this.enqueueTypewrite(userIdx + 1, text2, lid2);
1910
2538
  this.updateHistoryCache();
1911
2539
  }
2540
+ /** How a bg task maps onto a collapsed row: the row's own key (storage path
2541
+ * when known, else the filename), scoped to the chat it belongs to. A storage
2542
+ * path is project-relative ("report.xlsx"), and ONE ChatSession serves every
2543
+ * project — unscoped, stopping a file in one project would silently suppress
2544
+ * the same filename's continuations in another. */
2545
+ _indexKeyOf(entry) {
2546
+ if (!entry) return "";
2547
+ var file = entry.storagePath || entry.filename;
2548
+ if (!file) return "";
2549
+ return entry.serviceId + "#" + entry.platform + "|" + file;
2550
+ }
2551
+ /**
2552
+ * Reconcile the bg queue with the files the user has stopped.
2553
+ *
2554
+ * A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
2555
+ * Reindex from the file manager — so it LIFTS the stop: the key is a storage
2556
+ * path, and without this an earlier cancel would silently kill every future
2557
+ * index of the same path. A continuation of a stopped file is dropped instead,
2558
+ * covering the pass that was dispatched in the moment before the cancel landed.
2559
+ */
2560
+ _applyIndexCancellations() {
2561
+ if (!this.cancelledIndexKeys.size) return;
2562
+ for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
2563
+ var entry = this.bgTaskQueue[i];
2564
+ var key = this._indexKeyOf(entry);
2565
+ if (!key || !this.cancelledIndexKeys.has(key)) continue;
2566
+ if (!entry.resumePass) {
2567
+ this.cancelledIndexKeys.delete(key);
2568
+ continue;
2569
+ }
2570
+ this.bgTaskQueue.splice(i, 1);
2571
+ this._stopPoll(entry.id);
2572
+ this._cancelServerItem(entry.id);
2573
+ }
2574
+ }
2575
+ /**
2576
+ * Cancel any live pass of a stopped file that turned up on its own.
2577
+ *
2578
+ * The client is not the only thing that continues a file: for PDFs and (when
2579
+ * windowed indexing is on) text/grid files the WORKER enqueues the next window
2580
+ * itself, and that pass reaches the chat through the history poll, never
2581
+ * through bgTaskQueue. The worker's own gate stops the chain when the running
2582
+ * row is cancelled — but if the row had already finished when the user hit
2583
+ * stop, the next window was queued a moment earlier and still arrives. Stop it
2584
+ * here rather than making the user hit stop again.
2585
+ *
2586
+ * Runs from drainBgTaskQueue, which both clients call after a history load.
2587
+ */
2588
+ _sweepCancelledIndexing() {
2589
+ if (!this.cancelledIndexKeys.size) return;
2590
+ var self = this;
2591
+ var chatKey = this.getHistoryCacheKey();
2592
+ var targets = [];
2593
+ this.state.messages.forEach(function(m, i) {
2594
+ if (!m.isBackgroundTask || m.role !== "user" || !m._serverItemId) return;
2595
+ if (m._cancelling || m.isSendingToServer) return;
2596
+ if (!(m.isPendingQueued || m.isPendingInProcess)) return;
2597
+ var ref = m._indexFile;
2598
+ var file = ref && (ref.path || ref.name);
2599
+ if (!file || !self.cancelledIndexKeys.has(chatKey + "|" + file)) return;
2600
+ targets.push({ msg: m, idx: i });
2601
+ });
2602
+ targets.forEach(function(t) {
2603
+ var idx = self.state.messages.indexOf(t.msg);
2604
+ self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
2605
+ });
2606
+ }
2607
+ /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
2608
+ * cancelQueuedMessage, which drives one, has nothing to act on). */
2609
+ _cancelServerItem(serverId) {
2610
+ var id = this.host.getIdentity();
2611
+ if (!serverId || id.platform !== "claude" && id.platform !== "openai") return;
2612
+ var url = id.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
2613
+ Promise.resolve(this.host.cancelRequest({
2614
+ url,
2615
+ method: "POST",
2616
+ id: serverId,
2617
+ queue: (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX,
2618
+ service: id.serviceId,
2619
+ owner: id.owner
2620
+ })).catch(function() {
2621
+ });
2622
+ }
1912
2623
  // Inject "Indexing: <file>" bubbles for queued bg tasks + attach their polls.
1913
2624
  drainBgTaskQueue() {
1914
2625
  var self = this;
1915
2626
  var id = this.host.getIdentity();
1916
2627
  var svcId = id.serviceId, plat = id.platform;
1917
2628
  if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
2629
+ this._applyIndexCancellations();
2630
+ this._sweepCancelledIndexing();
1918
2631
  var presentIds = {};
1919
2632
  var pendingIds = {};
1920
2633
  this.state.messages.forEach(function(m) {
@@ -1930,39 +2643,70 @@ var ChatSession = class {
1930
2643
  }
1931
2644
  this.bgTaskQueue.forEach(function(entry) {
1932
2645
  if (entry.serviceId !== svcId || entry.platform !== plat) return;
1933
- if (presentIds[entry.id]) return;
1934
- var isRunning = entry.status === "running";
1935
- var userBubble = { role: "user", content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex), isBackgroundTask: true, _serverItemId: entry.id };
1936
- if (isRunning) userBubble.isPendingInProcess = true;
1937
- else userBubble.isPendingQueued = true;
1938
- self.state.messages.push(userBubble);
1939
- if (isRunning) {
1940
- self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2646
+ if (!presentIds[entry.id]) {
2647
+ var isRunning = entry.status === "running";
2648
+ var userBubble = {
2649
+ role: "user",
2650
+ content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex, !!entry.resumePass),
2651
+ isBackgroundTask: true,
2652
+ _serverItemId: entry.id,
2653
+ // Structured ref so this live pass groups with the same file's passes
2654
+ // rebuilt from history (see indexing_groups.buildChatDisplayList).
2655
+ _indexFile: {
2656
+ name: entry.filename,
2657
+ path: entry.storagePath,
2658
+ mime: entry.mime,
2659
+ size: entry.size,
2660
+ isReindex: !!entry.isReindex,
2661
+ continued: !!entry.resumePass
2662
+ }
2663
+ };
2664
+ if (isRunning) userBubble.isPendingInProcess = true;
2665
+ else userBubble.isPendingQueued = true;
2666
+ self.state.messages.push(userBubble);
2667
+ if (isRunning) {
2668
+ self.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id });
2669
+ }
2670
+ presentIds[entry.id] = true;
2671
+ self.host.notify();
2672
+ self.updateHistoryCache();
2673
+ self.host.scrollToBottomIfSticky(false);
1941
2674
  }
1942
- presentIds[entry.id] = true;
1943
- self.host.notify();
1944
- self.updateHistoryCache();
1945
- self.host.scrollToBottom(false);
1946
- if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1947
- self.historyItemPolls.set(entry.id, true);
2675
+ if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
1948
2676
  var capturedId = entry.id, capturedPlat = plat;
1949
2677
  var capturedEntry = entry;
1950
- entry.poll({ latency: POLL_INTERVAL }).then(function(response) {
2678
+ var wasStopped = false;
2679
+ var bp = entry.poll({ latency: POLL_INTERVAL });
2680
+ self._trackPoll(entry.id, "bg", bp);
2681
+ bp.then(function(response) {
2682
+ if (isPollStopped(response)) {
2683
+ wasStopped = true;
2684
+ return;
2685
+ }
1951
2686
  self.handleHistoryItemResolution(capturedId, response, capturedPlat);
1952
2687
  self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
1953
2688
  }).catch(function(err) {
1954
2689
  self.historyItemPolls.delete(capturedId);
1955
2690
  var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
2691
+ self._clearPendingUserBubble(capturedId);
1956
2692
  var bi = self.state.messages.findIndex(function(m) {
1957
2693
  return m.isPending && m._serverItemId === capturedId;
1958
2694
  });
1959
2695
  if (bi !== -1) {
1960
2696
  if (isNotExists) self.state.messages.splice(bi, 1);
1961
2697
  else self.state.messages[bi] = { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId };
1962
- self.host.notify();
1963
- self.updateHistoryCache();
2698
+ } else if (!isNotExists) {
2699
+ var ui = self.state.messages.findIndex(function(m) {
2700
+ return m.role === "user" && m._serverItemId === capturedId;
2701
+ });
2702
+ if (ui !== -1) {
2703
+ self.state.messages.splice(ui + 1, 0, { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId });
2704
+ }
1964
2705
  }
2706
+ self.host.notify();
2707
+ self.updateHistoryCache();
1965
2708
  }).then(function() {
2709
+ if (wasStopped) return;
1966
2710
  var qi = self.bgTaskQueue.findIndex(function(q) {
1967
2711
  return q.id === capturedId;
1968
2712
  });
@@ -1973,25 +2717,32 @@ var ChatSession = class {
1973
2717
  this.promoteNextBgQueuedToRunning();
1974
2718
  }
1975
2719
  // Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
1976
- // PDF) finished WITHOUT the completion marker, the agent ran out of room before reading
2720
+ // text) finished WITHOUT the completion marker, the agent ran out of room before reading
1977
2721
  // the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
1978
2722
  // already-saved records leave off. Additive + guarded so it never loops forever and
1979
2723
  // never breaks the resolution path.
2724
+ //
2725
+ // VISION files (PDFs rendered to page images) are NOT resumed here: the proxy worker
2726
+ // advances their page window itself, off the renderer's true page count. Driving them
2727
+ // from the browser is what used to lose pages on long documents - the chain lived in tab
2728
+ // memory (a reload or a closed tab ended it), and it stopped whenever the model claimed
2729
+ // completion, which on an 88-page file happened at page 15. Continuing to dispatch here
2730
+ // as well would now double-index every window.
1980
2731
  maybeResumeIndexing(entry, response, platform) {
1981
2732
  var self = this;
1982
2733
  try {
1983
2734
  if (!entry || !entry.storagePath) return;
2735
+ if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
1984
2736
  if (!isPagedReadFile(entry.filename, entry.mime)) return;
2737
+ if (isImageVisionFile(entry.filename, entry.mime)) return;
2738
+ if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
1985
2739
  if (isErrorResponseBody(response)) return;
1986
2740
  var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
1987
2741
  if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
1988
2742
  var pass = (entry.resumePass || 0) + 1;
1989
- var isVision = isImageVisionFile(entry.filename, entry.mime);
1990
- var maxPasses = isVision ? MAX_VISION_RESUME_PASSES : MAX_INDEXING_RESUME_PASSES;
1991
- if (pass > maxPasses) return;
2743
+ if (pass > MAX_INDEXING_RESUME_PASSES) return;
1992
2744
  var id = this.host.getIdentity();
1993
2745
  if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
1994
- var renderFrom = isVision ? pass * RENDER_PAGES_PER_WINDOW : void 0;
1995
2746
  notifyAgentContinueIndexing({
1996
2747
  platform: id.platform,
1997
2748
  model: id.model,
@@ -2000,7 +2751,6 @@ var ChatSession = class {
2000
2751
  userId: id.userId || id.serviceId,
2001
2752
  serviceName: id.serviceName,
2002
2753
  serviceDescription: id.serviceDescription,
2003
- renderFrom,
2004
2754
  attachment: {
2005
2755
  name: entry.filename,
2006
2756
  storagePath: entry.storagePath,
@@ -2040,6 +2790,7 @@ var ChatSession = class {
2040
2790
  loadHistory(fetchMore, token) {
2041
2791
  var self = this;
2042
2792
  var id = this.host.getIdentity();
2793
+ var loadKey = !id.serviceId || id.platform === "none" ? "" : id.serviceId + "#" + id.platform;
2043
2794
  if (token === void 0) token = this.state.gateRefreshToken;
2044
2795
  if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.serviceId) {
2045
2796
  return Promise.resolve();
@@ -2062,9 +2813,9 @@ var ChatSession = class {
2062
2813
  if (token !== self.state.gateRefreshToken) return;
2063
2814
  var chatList = history && Array.isArray(history.list) ? history.list : [];
2064
2815
  chatList.forEach(function(item) {
2065
- if (typeof item.queue_name === "string" && item.queue_name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX) {
2816
+ if (isBgIndexingQueue(item.queue_name)) {
2066
2817
  var userText = extractLastUserTextFromRequest(item.request_body);
2067
- if (typeof userText === "string" && userText.indexOf("A new file has just been uploaded") === 0) item._isBgTask = true;
2818
+ if (typeof userText === "string" && (userText.indexOf("A new file has just been uploaded") === 0 || userText.indexOf("CONTINUE indexing") === 0)) item._isBgTask = true;
2068
2819
  else item._isOnBgQueue = true;
2069
2820
  }
2070
2821
  });
@@ -2077,6 +2828,7 @@ var ChatSession = class {
2077
2828
  serviceId: id.serviceId,
2078
2829
  formatIndexingLabel: self.host.formatIndexingLabel
2079
2830
  }).messages;
2831
+ var keptOlderPages = false;
2080
2832
  if (fetchMore) {
2081
2833
  self.state.messages = mapped.concat(self.state.messages);
2082
2834
  } else {
@@ -2087,7 +2839,12 @@ var ChatSession = class {
2087
2839
  });
2088
2840
  var locallyCancelled = {};
2089
2841
  self.state.messages.forEach(function(m) {
2090
- if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = 1;
2842
+ if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
2843
+ });
2844
+ var inFlightCancel = {};
2845
+ self.state.messages.forEach(function(m) {
2846
+ if (!m._serverItemId) return;
2847
+ if (m._cancelling || m._cancelError) inFlightCancel[m._serverItemId] = m;
2091
2848
  });
2092
2849
  var mappedHasPendingAssistant = mapped.some(function(m) {
2093
2850
  return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
@@ -2096,6 +2853,7 @@ var ChatSession = class {
2096
2853
  for (var ri = 0; ri < self.state.messages.length; ri++) {
2097
2854
  var mm = self.state.messages[ri];
2098
2855
  if (mm.isBackgroundTask) continue;
2856
+ if (mm._ownerKey !== void 0 && mm._ownerKey !== loadKey) continue;
2099
2857
  if (mm._serverItemId && serverIds[mm._serverItemId]) continue;
2100
2858
  if (!mm._serverItemId) {
2101
2859
  if (mappedHasPendingAssistant) continue;
@@ -2106,7 +2864,22 @@ var ChatSession = class {
2106
2864
  }
2107
2865
  }
2108
2866
  }
2109
- self.state.messages = mapped;
2867
+ var oldestInPage1 = void 0;
2868
+ mapped.forEach(function(m) {
2869
+ var sid = m._serverItemId;
2870
+ if (typeof sid !== "string") return;
2871
+ if (oldestInPage1 === void 0 || sid < oldestInPage1) oldestInPage1 = sid;
2872
+ });
2873
+ var sharesPage1 = self.state.messages.some(function(m) {
2874
+ return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
2875
+ });
2876
+ var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
2877
+ if (typeof m._serverItemId !== "string") return false;
2878
+ if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
2879
+ return m._serverItemId < oldestInPage1;
2880
+ });
2881
+ keptOlderPages = retainedOlder.length > 0;
2882
+ self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
2110
2883
  rescued.forEach(function(m) {
2111
2884
  self.state.messages.push(m);
2112
2885
  });
@@ -2114,19 +2887,38 @@ var ChatSession = class {
2114
2887
  for (var ci = 0; ci < self.state.messages.length; ci++) {
2115
2888
  var c = self.state.messages[ci];
2116
2889
  if (!c._serverItemId || !locallyCancelled[c._serverItemId] || c.isCancelled) continue;
2117
- self.state.messages[ci] = { role: "user", content: c.content, isCancelled: true, _serverItemId: c._serverItemId };
2890
+ self.state.messages[ci] = {
2891
+ role: "user",
2892
+ content: c.content,
2893
+ isCancelled: true,
2894
+ _serverItemId: c._serverItemId,
2895
+ isBackgroundTask: c.isBackgroundTask,
2896
+ _indexFile: c._indexFile,
2897
+ _useBgQueue: c._useBgQueue,
2898
+ _ownerKey: c._ownerKey
2899
+ };
2118
2900
  if (ci + 1 < self.state.messages.length && self.state.messages[ci + 1].isPending && self.state.messages[ci + 1]._serverItemId === c._serverItemId) {
2119
2901
  self.state.messages.splice(ci + 1, 1);
2120
2902
  }
2121
2903
  }
2122
2904
  }
2905
+ for (var fi = 0; fi < self.state.messages.length; fi++) {
2906
+ var fm = self.state.messages[fi];
2907
+ var was = fm._serverItemId && inFlightCancel[fm._serverItemId];
2908
+ if (!was || fm.isCancelled) continue;
2909
+ if (!(fm.isPendingQueued || fm.isPendingInProcess || fm.isPending)) continue;
2910
+ if (was._cancelling) fm._cancelling = true;
2911
+ if (was._cancelError) fm._cancelError = was._cancelError;
2912
+ }
2123
2913
  }
2124
- self.state.historyEndOfList = !!(history && history.endOfList);
2125
- self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2126
- var clearedAt = self.host.getClearedAt();
2127
- if (clearedAt && chatList.length > 0) {
2128
- var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2129
- if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2914
+ if (!keptOlderPages) {
2915
+ self.state.historyEndOfList = !!(history && history.endOfList);
2916
+ self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
2917
+ var clearedAt = self.host.getClearedAt();
2918
+ if (clearedAt && chatList.length > 0) {
2919
+ var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
2920
+ if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
2921
+ }
2130
2922
  }
2131
2923
  if (self.state.historyRequestToken === token) {
2132
2924
  self.state.loadingHistory = false;
@@ -2140,11 +2932,12 @@ var ChatSession = class {
2140
2932
  if (!item.poll || !item.id) return;
2141
2933
  if (self.historyItemPolls.has(item.id)) return;
2142
2934
  if (self.pendingAgentRequests[self.getHistoryCacheKey()] && !item._isBgTask && !item._isOnBgQueue) return;
2143
- self.historyItemPolls.set(item.id, true);
2935
+ if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
2144
2936
  var capturedId = item.id;
2145
2937
  var pp = item.poll({
2146
2938
  latency: POLL_INTERVAL,
2147
2939
  onResponse: function(response) {
2940
+ if (isPollStopped(response)) return;
2148
2941
  self.handleHistoryItemResolution(capturedId, response, platform);
2149
2942
  },
2150
2943
  onError: function(err) {
@@ -2154,18 +2947,25 @@ var ChatSession = class {
2154
2947
  return m.isPending && m._serverItemId === capturedId;
2155
2948
  });
2156
2949
  if (isNotExists) {
2157
- var isBg = aIdx !== -1 ? !!self.state.messages[aIdx].isBackgroundTask : false;
2950
+ var uIdx = self.state.messages.findIndex(function(m) {
2951
+ return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2952
+ });
2953
+ var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
2158
2954
  if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
2159
2955
  if (!isBg) {
2160
- var uIdx = self.state.messages.findIndex(function(m) {
2161
- return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
2162
- });
2163
2956
  if (uIdx !== -1) {
2164
2957
  var ex = self.state.messages[uIdx];
2165
2958
  self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
2166
2959
  }
2167
2960
  self.cancelledServerIds.delete(capturedId);
2168
2961
  self.promoteNextQueuedToRunning();
2962
+ } else if (uIdx !== -1) {
2963
+ var bex = self.state.messages[uIdx];
2964
+ var bcancelled = { role: "user", content: bex.content, isCancelled: true, isBackgroundTask: true, _serverItemId: bex._serverItemId };
2965
+ if (bex._indexFile) bcancelled._indexFile = bex._indexFile;
2966
+ if (bex._useBgQueue) bcancelled._useBgQueue = true;
2967
+ self.state.messages[uIdx] = bcancelled;
2968
+ self.promoteNextBgQueuedToRunning();
2169
2969
  }
2170
2970
  self.host.notify();
2171
2971
  self.updateHistoryCache();
@@ -2180,12 +2980,13 @@ var ChatSession = class {
2180
2980
  }
2181
2981
  }
2182
2982
  });
2983
+ self._trackPoll(capturedId, item._isBgTask || item._isOnBgQueue ? "bg" : "fg", pp);
2183
2984
  if (pp && pp.catch) pp.catch(function() {
2184
2985
  });
2185
2986
  });
2186
2987
  self.drainBgTaskQueue();
2187
2988
  }
2188
- if (!fetchMore) return self.host.scrollToBottom();
2989
+ if (!fetchMore) return self.host.scrollToBottomIfSticky();
2189
2990
  }).catch(function(err) {
2190
2991
  console.warn("[chat-engine] getChatHistory failed", err);
2191
2992
  }).then(function() {
@@ -2195,6 +2996,7 @@ var ChatSession = class {
2195
2996
  self.state.loadingOlderHistory = false;
2196
2997
  if (wasLoading) self.host.notify();
2197
2998
  }
2999
+ if (self.host.onHistoryLoaded) self.host.onHistoryLoaded(!!fetchMore, token);
2198
3000
  });
2199
3001
  }
2200
3002
  // --- attachment upload orchestration ---------------------------------
@@ -2392,6 +3194,189 @@ var ChatSession = class {
2392
3194
  }
2393
3195
  };
2394
3196
 
3197
+ // src/engine/indexing_groups.ts
3198
+ var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
3199
+ var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
3200
+ function parseIndexingLabel(content) {
3201
+ if (typeof content !== "string" || !content) return null;
3202
+ var firstLine = content.split("\n")[0].trim();
3203
+ var m = firstLine.match(INDEXING_LABEL_RE);
3204
+ if (!m) return null;
3205
+ var head = m[3].split(" \xB7 ")[0].trim();
3206
+ var link = head.match(LEADING_MD_LINK_RE);
3207
+ var name = link ? link[1].trim() : head;
3208
+ if (!name) return null;
3209
+ return {
3210
+ name,
3211
+ path: link ? link[2].trim() : void 0,
3212
+ continued: !!m[2],
3213
+ isReindex: !!m[1]
3214
+ };
3215
+ }
3216
+ function readFileRef(msg) {
3217
+ var ref = msg && msg._indexFile;
3218
+ if (ref && (ref.path || ref.name)) {
3219
+ return {
3220
+ name: ref.name || ref.path || "",
3221
+ path: ref.path,
3222
+ mime: ref.mime,
3223
+ size: ref.size,
3224
+ isReindex: ref.isReindex,
3225
+ continued: !!ref.continued
3226
+ };
3227
+ }
3228
+ var parsed = parseIndexingLabel(msg && msg.content);
3229
+ if (!parsed) return null;
3230
+ return {
3231
+ name: parsed.name,
3232
+ path: parsed.path,
3233
+ isReindex: parsed.isReindex,
3234
+ continued: parsed.continued
3235
+ };
3236
+ }
3237
+ function isPendingMsg(m) {
3238
+ return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
3239
+ }
3240
+ function buildChatDisplayList(messages, opts) {
3241
+ var list = Array.isArray(messages) ? messages : [];
3242
+ var hasMoreHistory = !!(opts && opts.hasMoreHistory);
3243
+ var groups = {};
3244
+ var order = [];
3245
+ var runOfIndex = new Array(list.length);
3246
+ var runByItemId = {};
3247
+ var keyByName = {};
3248
+ var openRunOfKey = {};
3249
+ var runsOfKey = {};
3250
+ var keyOfRun = {};
3251
+ var runSeq = 0;
3252
+ for (var i = 0; i < list.length; i++) {
3253
+ var msg = list[i];
3254
+ if (!msg || !msg.isBackgroundTask) continue;
3255
+ var runId;
3256
+ var ref = msg.role === "user" ? readFileRef(msg) : null;
3257
+ if (ref) {
3258
+ var key = ref.path || keyByName[ref.name] || ref.name;
3259
+ if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
3260
+ runId = openRunOfKey[key];
3261
+ if (!runId) {
3262
+ runId = "run" + runSeq++;
3263
+ openRunOfKey[key] = runId;
3264
+ keyOfRun[runId] = key;
3265
+ (runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
3266
+ }
3267
+ } else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
3268
+ runId = runByItemId[msg._serverItemId];
3269
+ } else if (msg.role !== "user") {
3270
+ runId = runOfIndex[i - 1];
3271
+ }
3272
+ if (!runId) continue;
3273
+ var g = groups[runId];
3274
+ if (!g) {
3275
+ var fileKey = keyOfRun[runId];
3276
+ g = groups[runId] = {
3277
+ key: fileKey,
3278
+ runKey: runId,
3279
+ // provisional; renumbered newest-first below
3280
+ name: ref ? ref.name : fileKey,
3281
+ path: ref ? ref.path : void 0,
3282
+ mime: ref ? ref.mime : void 0,
3283
+ size: ref ? ref.size : void 0,
3284
+ isReindex: !!(ref && ref.isReindex),
3285
+ members: [],
3286
+ passCount: 0,
3287
+ status: "done",
3288
+ cancellableIds: [],
3289
+ cancelling: false,
3290
+ mayHaveOlder: false,
3291
+ anchorIndex: i
3292
+ };
3293
+ order.push(runId);
3294
+ }
3295
+ if (ref) {
3296
+ if (ref.name) g.name = ref.name;
3297
+ if (ref.path) g.path = ref.path;
3298
+ if (ref.mime) g.mime = ref.mime;
3299
+ if (typeof ref.size === "number") g.size = ref.size;
3300
+ if (ref.isReindex) g.isReindex = true;
3301
+ if (!ref.continued) g.mayHaveOlder = false;
3302
+ g.passCount++;
3303
+ }
3304
+ g.members.push({ msg, index: i });
3305
+ g.anchorIndex = i;
3306
+ runOfIndex[i] = runId;
3307
+ if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
3308
+ if (ref && ref.name) keyByName[ref.name] = g.key;
3309
+ }
3310
+ for (var rk in runsOfKey) {
3311
+ var runIds = runsOfKey[rk];
3312
+ for (var ri = 0; ri < runIds.length; ri++) {
3313
+ var grpR = groups[runIds[ri]];
3314
+ if (!grpR) continue;
3315
+ var first = grpR.members[0];
3316
+ var firstId = first && first.msg && (first.msg._serverItemId || first.msg._localId);
3317
+ grpR.runKey = rk + "#" + (firstId || "n" + ri);
3318
+ }
3319
+ }
3320
+ for (var oi = 0; oi < order.length; oi++) {
3321
+ var grp = groups[order[oi]];
3322
+ var lastSettled = -1;
3323
+ for (var si = 0; si < grp.members.length; si++) {
3324
+ if (!isPendingMsg(grp.members[si].msg)) lastSettled = si;
3325
+ }
3326
+ var active = false;
3327
+ for (var mi = lastSettled + 1; mi < grp.members.length; mi++) {
3328
+ if (isPendingMsg(grp.members[mi].msg)) {
3329
+ active = true;
3330
+ break;
3331
+ }
3332
+ }
3333
+ for (var xi = 0; xi < grp.members.length; xi++) {
3334
+ if (grp.members[xi].msg._cancelling) {
3335
+ grp.cancelling = true;
3336
+ break;
3337
+ }
3338
+ }
3339
+ var seenIds = {};
3340
+ for (var ci = 0; ci < grp.members.length; ci++) {
3341
+ var cm = grp.members[ci].msg;
3342
+ if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
3343
+ if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
3344
+ if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
3345
+ if (ci < lastSettled) continue;
3346
+ if (seenIds[cm._serverItemId]) continue;
3347
+ seenIds[cm._serverItemId] = true;
3348
+ grp.cancellableIds.push(cm._serverItemId);
3349
+ }
3350
+ if (active) {
3351
+ grp.status = "active";
3352
+ } else {
3353
+ var last = grp.members[grp.members.length - 1].msg;
3354
+ grp.status = last.isError ? "error" : last.isCancelled ? "cancelled" : "done";
3355
+ }
3356
+ var sawFirstPass = false;
3357
+ for (var pi = 0; pi < grp.members.length; pi++) {
3358
+ var pm = grp.members[pi].msg;
3359
+ if (pm.role !== "user") continue;
3360
+ var pref = readFileRef(pm);
3361
+ if (pref && !pref.continued) {
3362
+ sawFirstPass = true;
3363
+ break;
3364
+ }
3365
+ }
3366
+ grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
3367
+ }
3368
+ var out = [];
3369
+ for (var j = 0; j < list.length; j++) {
3370
+ var r = runOfIndex[j];
3371
+ if (r === void 0) {
3372
+ out.push({ kind: "message", msg: list[j], index: j });
3373
+ continue;
3374
+ }
3375
+ if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
3376
+ }
3377
+ return out;
3378
+ }
3379
+
2395
3380
  exports.BG_INDEXING_QUEUE_SUFFIX = BG_INDEXING_QUEUE_SUFFIX;
2396
3381
  exports.CLAUDE_PER_REQUEST_INPUT_CAP = CLAUDE_PER_REQUEST_INPUT_CAP;
2397
3382
  exports.CONTEXT_WINDOW_BY_MODEL = CONTEXT_WINDOW_BY_MODEL;
@@ -2401,29 +3386,39 @@ exports.DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_MODEL;
2401
3386
  exports.DEFAULT_OPENAI_MODEL = DEFAULT_OPENAI_MODEL;
2402
3387
  exports.EXPIRED_ATTACHMENT_URL_HOST = EXPIRED_ATTACHMENT_URL_HOST;
2403
3388
  exports.EXPIRED_ATTACHMENT_URL_ORIGIN = EXPIRED_ATTACHMENT_URL_ORIGIN;
3389
+ exports.EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = EXPIRED_LINK_REFRESH_EXPIRES_SECONDS;
3390
+ exports.HISTORY_FILL_SLACK_PX = HISTORY_FILL_SLACK_PX;
2404
3391
  exports.HISTORY_TOKEN_BUDGET = HISTORY_TOKEN_BUDGET;
2405
3392
  exports.LINK_LABEL_MAX_DISPLAY_CHARS = LINK_LABEL_MAX_DISPLAY_CHARS;
3393
+ exports.LINK_REFRESH_WINDOW_MS = LINK_REFRESH_WINDOW_MS;
3394
+ exports.MAX_HISTORY_FILL_PAGES = MAX_HISTORY_FILL_PAGES;
2406
3395
  exports.MAX_HISTORY_MESSAGES = MAX_HISTORY_MESSAGES;
2407
3396
  exports.MAX_PARSED_CONTENT_CHARS = MAX_PARSED_CONTENT_CHARS;
2408
3397
  exports.MCP_NAME = MCP_NAME;
2409
3398
  exports.MIN_INPUT_TOKEN_BUDGET = MIN_INPUT_TOKEN_BUDGET;
2410
3399
  exports.OUTPUT_TOKEN_RESERVE = OUTPUT_TOKEN_RESERVE;
2411
3400
  exports.POLL_INTERVAL = POLL_INTERVAL;
3401
+ exports.RENDER_FROM_TOKEN = RENDER_FROM_TOKEN;
2412
3402
  exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
2413
3403
  exports.buildBoundedChatMessages = buildBoundedChatMessages;
3404
+ exports.buildChatDisplayList = buildChatDisplayList;
2414
3405
  exports.buildChatSystemPrompt = buildChatSystemPrompt;
2415
3406
  exports.buildDisplayExpiredAttachmentHref = buildDisplayExpiredAttachmentHref;
2416
3407
  exports.buildIndexingContinueMessage = buildIndexingContinueMessage;
3408
+ exports.buildIndexingRenderContinueTemplate = buildIndexingRenderContinueTemplate;
2417
3409
  exports.buildIndexingRenderMessage = buildIndexingRenderMessage;
2418
3410
  exports.buildIndexingSystemPrompt = buildIndexingSystemPrompt;
2419
3411
  exports.buildIndexingUserMessage = buildIndexingUserMessage;
3412
+ exports.buildIndexingWindowMessage = buildIndexingWindowMessage;
2420
3413
  exports.callClaudeWithMcp = callClaudeWithMcp;
2421
3414
  exports.callClaudeWithPublicMcp = callClaudeWithPublicMcp;
2422
3415
  exports.callOpenAIWithPublicMcp = callOpenAIWithPublicMcp;
2423
3416
  exports.chatEngineConfig = chatEngineConfig;
3417
+ exports.classifyInlineLink = classifyInlineLink;
2424
3418
  exports.clearAttachmentParsers = clearAttachmentParsers;
2425
3419
  exports.composeUserMessage = composeUserMessage;
2426
3420
  exports.configureChatEngine = configureChatEngine;
3421
+ exports.createHistoryFiller = createHistoryFiller;
2427
3422
  exports.createInlineLinkRegex = createInlineLinkRegex;
2428
3423
  exports.encodePathSegments = encodePathSegments;
2429
3424
  exports.estimateMessageTokens = estimateMessageTokens;
@@ -2432,6 +3427,7 @@ exports.extractClaudeText = extractClaudeText;
2432
3427
  exports.extractLastUserTextFromRequest = extractLastUserTextFromRequest;
2433
3428
  exports.extractOpenAIText = extractOpenAIText;
2434
3429
  exports.extractRemotePathFromAttachmentHref = extractRemotePathFromAttachmentHref;
3430
+ exports.fillHistoryViewport = fillHistoryViewport;
2435
3431
  exports.filterListByClearHorizon = filterListByClearHorizon;
2436
3432
  exports.findAttachmentParser = findAttachmentParser;
2437
3433
  exports.getAttachmentParsers = getAttachmentParsers;
@@ -2441,7 +3437,9 @@ exports.getErrorMessage = getErrorMessage;
2441
3437
  exports.getExpiredAttachmentVisiblePath = getExpiredAttachmentVisiblePath;
2442
3438
  exports.groupAttachmentFailures = groupAttachmentFailures;
2443
3439
  exports.isAuthExpiredError = isAuthExpiredError;
3440
+ exports.isBgIndexingQueue = isBgIndexingQueue;
2444
3441
  exports.isErrorResponseBody = isErrorResponseBody;
3442
+ exports.isHttpUrlLike = isHttpUrlLike;
2445
3443
  exports.isNonRetryableRequestError = isNonRetryableRequestError;
2446
3444
  exports.isOfficeFile = isOfficeFile;
2447
3445
  exports.isServerExtractable = isServerExtractable;
@@ -2452,9 +3450,13 @@ exports.makeExtractPlaceholder = makeExtractPlaceholder;
2452
3450
  exports.mapHistoryListToMessages = mapHistoryListToMessages;
2453
3451
  exports.normalizeAttachmentPathCandidate = normalizeAttachmentPathCandidate;
2454
3452
  exports.normalizeTextContent = normalizeTextContent;
3453
+ exports.normalizeTrailingInlineToken = normalizeTrailingInlineToken;
2455
3454
  exports.notifyAgentSaveAttachment = notifyAgentSaveAttachment;
2456
3455
  exports.parseAttachmentContent = parseAttachmentContent;
3456
+ exports.parseIndexingLabel = parseIndexingLabel;
3457
+ exports.readExpiredAttachmentHref = readExpiredAttachmentHref;
2457
3458
  exports.registerAttachmentParser = registerAttachmentParser;
3459
+ exports.repairUrlWhitespace = repairUrlWhitespace;
2458
3460
  exports.safeDecodeURIComponent = safeDecodeURIComponent;
2459
3461
  exports.sanitizeAttachmentLinksForHistory = sanitizeAttachmentLinksForHistory;
2460
3462
  exports.stripFileBlocksFromHistory = stripFileBlocksFromHistory;