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