bunnyquery 1.6.2 → 1.8.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/README.md +157 -29
- package/bunnyquery.css +134 -0
- package/bunnyquery.js +1057 -120
- package/dist/engine.cjs +784 -42
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +455 -4
- package/dist/engine.d.ts +455 -4
- package/dist/engine.mjs +769 -43
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/history.ts +30 -2
- package/src/engine/host.ts +34 -1
- package/src/engine/index.ts +25 -1
- package/src/engine/indexing_groups.ts +389 -0
- package/src/engine/links.ts +343 -9
- package/src/engine/prompts/chat_system_prompt.ts +4 -1
- package/src/engine/prompts/indexing_user_message.ts +4 -0
- package/src/engine/requests.ts +1 -1
- package/src/engine/session.ts +412 -33
- package/src/engine/time.ts +34 -0
- package/src/engine/viewport_fill.ts +186 -0
- package/styles/chat.css +134 -0
package/bunnyquery.js
CHANGED
|
@@ -280,11 +280,14 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
|
|
|
280
280
|
You are a dedicated assistant for the project ID: "${formattedServiceId}".
|
|
281
281
|
Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage.
|
|
282
282
|
Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
|
|
283
|
+
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records that usually share a table. So a question about the data almost always spans many records across several files. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY relevant table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset.
|
|
284
|
+
Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
|
|
285
|
+
Embedded values: a search term is often stored inside a larger string. A merchant "GODADDY" appears as "DNH*GODADDY#4070277042", and a card as "4140****2941". Server-side index and tag filters match only exact values or leading prefixes, not substrings, so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
|
|
283
286
|
File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
|
|
284
287
|
- 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.
|
|
285
288
|
- 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.
|
|
286
289
|
- 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.
|
|
287
|
-
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.
|
|
290
|
+
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.
|
|
288
291
|
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.
|
|
289
292
|
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:
|
|
290
293
|
\`\`\`filename.csv
|
|
@@ -409,6 +412,8 @@ ${placeholder}
|
|
|
409
412
|
|
|
410
413
|
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.
|
|
411
414
|
|
|
415
|
+
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.
|
|
416
|
+
|
|
412
417
|
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.`;
|
|
413
418
|
}
|
|
414
419
|
function buildIndexingContinueMessage(attachment) {
|
|
@@ -519,8 +524,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
519
524
|
var EXPIRED_ATTACHMENT_URL_HOST = "_expired_.url";
|
|
520
525
|
var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
|
|
521
526
|
var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
|
|
527
|
+
var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
|
|
528
|
+
var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
|
|
522
529
|
function createInlineLinkRegex() {
|
|
523
|
-
return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]
|
|
530
|
+
return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
|
|
524
531
|
}
|
|
525
532
|
function safeDecodeURIComponent(v) {
|
|
526
533
|
try {
|
|
@@ -580,18 +587,161 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
580
587
|
return false;
|
|
581
588
|
}
|
|
582
589
|
}
|
|
590
|
+
function readExpiredAttachmentHref(href) {
|
|
591
|
+
if (!href) return null;
|
|
592
|
+
try {
|
|
593
|
+
var parsed = new URL(href);
|
|
594
|
+
if (parsed.hostname !== EXPIRED_ATTACHMENT_URL_HOST) return null;
|
|
595
|
+
return normalizeAttachmentPathCandidate(parsed.pathname || "") || null;
|
|
596
|
+
} catch (e) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
583
600
|
function sanitizeAttachmentLinksForHistory(content, serviceId, forAssistant) {
|
|
584
601
|
if (!content) return content;
|
|
585
602
|
if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
|
|
586
603
|
return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
|
|
587
|
-
if (
|
|
604
|
+
if (!isServiceDbAttachmentHref(href, serviceId)) return _m;
|
|
588
605
|
var remotePath = extractRemotePathFromAttachmentHref(href, serviceId);
|
|
589
|
-
var
|
|
590
|
-
|
|
591
|
-
if (!fullPath) return forAssistant ? _m : "[" + label + "](" + EXPIRED_ATTACHMENT_URL_ORIGIN + "/file)";
|
|
606
|
+
var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
|
|
607
|
+
if (!fullPath) return _m;
|
|
592
608
|
return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
|
|
593
609
|
});
|
|
594
610
|
}
|
|
611
|
+
function isHttpUrlLike(target) {
|
|
612
|
+
return /^https?:\/\//i.test((target || "").trim());
|
|
613
|
+
}
|
|
614
|
+
function repairUrlWhitespace(href) {
|
|
615
|
+
if (!href || !/\s/.test(href)) return href;
|
|
616
|
+
var stripped = href.replace(/\s+/g, "");
|
|
617
|
+
if (/^https?:\/\/[^/\s]+\/download\/[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)?$/i.test(stripped)) return stripped;
|
|
618
|
+
return href.trim().replace(/\s/g, "%20");
|
|
619
|
+
}
|
|
620
|
+
function repairUrlEntities(href) {
|
|
621
|
+
if (!href || href.indexOf("&") === -1) return href;
|
|
622
|
+
var out = href, prev = "";
|
|
623
|
+
while (out !== prev) {
|
|
624
|
+
prev = out;
|
|
625
|
+
out = out.replace(/&/gi, "&").replace(/�*38;/g, "&").replace(/�*26;/gi, "&");
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function normalizeTrailingInlineToken(value) {
|
|
630
|
+
if (!value) return value;
|
|
631
|
+
var out = value.replace(/[.,;:!?]+$/, "");
|
|
632
|
+
var trimUnmatched = function(openCh, closeCh) {
|
|
633
|
+
while (out.charAt(out.length - 1) === closeCh) {
|
|
634
|
+
var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
|
|
635
|
+
var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
|
|
636
|
+
if (closeCount > openCount) out = out.slice(0, -1);
|
|
637
|
+
else break;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
trimUnmatched("(", ")");
|
|
641
|
+
trimUnmatched("[", "]");
|
|
642
|
+
trimUnmatched("{", "}");
|
|
643
|
+
out = out.replace(/[`'"*>]+$/, "");
|
|
644
|
+
return out;
|
|
645
|
+
}
|
|
646
|
+
function classifyInlineLink(full, groups, ctx) {
|
|
647
|
+
var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
|
|
648
|
+
var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
|
|
649
|
+
var fresh = function(expiredHref) {
|
|
650
|
+
return ctx.resolveFreshHref ? ctx.resolveFreshHref(expiredHref) : void 0;
|
|
651
|
+
};
|
|
652
|
+
var isDbHost = function(url) {
|
|
653
|
+
return !!dbHostPrefix && url.toLowerCase().indexOf(dbHostPrefix) === 0;
|
|
654
|
+
};
|
|
655
|
+
var asStoredFile = function(remotePath2, label) {
|
|
656
|
+
if (!remotePath2) return null;
|
|
657
|
+
var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
|
|
658
|
+
var cached = fresh(expiredHref);
|
|
659
|
+
return {
|
|
660
|
+
part: {
|
|
661
|
+
type: "link",
|
|
662
|
+
label: truncateLabelForDisplay(label),
|
|
663
|
+
fullLabel: label,
|
|
664
|
+
href: cached || expiredHref,
|
|
665
|
+
expired: !cached,
|
|
666
|
+
expiredHref,
|
|
667
|
+
remotePath: remotePath2
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
if (g1) {
|
|
672
|
+
var rawPath = normalizeTrailingInlineToken(g1);
|
|
673
|
+
var tail = full.slice(("src::" + rawPath).length);
|
|
674
|
+
var srcIsUrl = isHttpUrlLike(rawPath);
|
|
675
|
+
if (srcIsUrl && !isDbHost(rawPath) && !readExpiredAttachmentHref(rawPath)) {
|
|
676
|
+
var srcUrl = repairUrlEntities(rawPath);
|
|
677
|
+
return {
|
|
678
|
+
part: { type: "link", label: truncateLabelForDisplay(srcUrl), fullLabel: srcUrl, href: srcUrl, expired: false },
|
|
679
|
+
tail
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.serviceId) || normalizeAttachmentPathCandidate(rawPath) : normalizeAttachmentPathCandidate(rawPath));
|
|
683
|
+
var srcBuilt = asStoredFile(srcPath, srcPath);
|
|
684
|
+
return srcBuilt ? { part: srcBuilt.part, tail } : null;
|
|
685
|
+
}
|
|
686
|
+
if (g4 && g5) {
|
|
687
|
+
var dbTarget = /^db:(.+)$/i.exec(g5.trim());
|
|
688
|
+
if (dbTarget) {
|
|
689
|
+
var declared = asStoredFile(normalizeAttachmentPathCandidate(dbTarget[1]), g4);
|
|
690
|
+
if (!declared) return null;
|
|
691
|
+
declared.part.label = truncateLabelForDisplay(g4);
|
|
692
|
+
declared.part.fullLabel = g4;
|
|
693
|
+
return declared;
|
|
694
|
+
}
|
|
695
|
+
if (isHttpUrlLike(g5)) {
|
|
696
|
+
return classifyInlineLink(full, [void 0, g4, repairUrlWhitespace(g5), void 0, void 0, void 0], ctx);
|
|
697
|
+
}
|
|
698
|
+
var trimmedTarget = g5.trim();
|
|
699
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmedTarget) || trimmedTarget.charAt(0) === "#") {
|
|
700
|
+
return {
|
|
701
|
+
part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: trimmedTarget, expired: false }
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
var built = asStoredFile(normalizeAttachmentPathCandidate(g5), g4);
|
|
705
|
+
if (!built) return null;
|
|
706
|
+
built.part.label = truncateLabelForDisplay(g4);
|
|
707
|
+
built.part.fullLabel = g4;
|
|
708
|
+
return built;
|
|
709
|
+
}
|
|
710
|
+
var originalHref = g3 || g6 || "";
|
|
711
|
+
if (!originalHref) return null;
|
|
712
|
+
originalHref = repairUrlEntities(originalHref);
|
|
713
|
+
var urlTail;
|
|
714
|
+
if (!g3 && g6) {
|
|
715
|
+
var trimmedUrl = normalizeTrailingInlineToken(originalHref);
|
|
716
|
+
if (trimmedUrl !== originalHref) urlTail = originalHref.slice(trimmedUrl.length);
|
|
717
|
+
originalHref = trimmedUrl;
|
|
718
|
+
}
|
|
719
|
+
var withTail = function(r) {
|
|
720
|
+
return urlTail ? { part: r.part, tail: urlTail } : r;
|
|
721
|
+
};
|
|
722
|
+
var urlLabel = g2 || originalHref;
|
|
723
|
+
var carried = readExpiredAttachmentHref(originalHref);
|
|
724
|
+
if (carried) {
|
|
725
|
+
var carriedBuilt = asStoredFile(carried, g2 || carried);
|
|
726
|
+
if (carriedBuilt) {
|
|
727
|
+
if (g2) {
|
|
728
|
+
carriedBuilt.part.label = truncateLabelForDisplay(g2);
|
|
729
|
+
carriedBuilt.part.fullLabel = g2;
|
|
730
|
+
}
|
|
731
|
+
return withTail(carriedBuilt);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (isServiceDbAttachmentHref(originalHref, ctx.serviceId)) {
|
|
735
|
+
var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.serviceId);
|
|
736
|
+
if (remotePath) {
|
|
737
|
+
var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
|
|
738
|
+
if (dbBuilt) return withTail(dbBuilt);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return withTail({
|
|
742
|
+
part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
|
|
743
|
+
});
|
|
744
|
+
}
|
|
595
745
|
function truncateLabelForDisplay(label) {
|
|
596
746
|
if (!label) return label;
|
|
597
747
|
if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
|
|
@@ -659,6 +809,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
659
809
|
};
|
|
660
810
|
}
|
|
661
811
|
|
|
812
|
+
// src/engine/time.ts
|
|
813
|
+
function wallClockNow() {
|
|
814
|
+
return Date.now();
|
|
815
|
+
}
|
|
816
|
+
function formatChatTimestamp(ms) {
|
|
817
|
+
if (typeof ms !== "number" || !isFinite(ms) || ms <= 0) return "";
|
|
818
|
+
try {
|
|
819
|
+
return new Date(ms).toLocaleString(void 0, {
|
|
820
|
+
year: "numeric",
|
|
821
|
+
month: "short",
|
|
822
|
+
day: "numeric",
|
|
823
|
+
hour: "numeric",
|
|
824
|
+
minute: "2-digit",
|
|
825
|
+
second: "2-digit"
|
|
826
|
+
});
|
|
827
|
+
} catch (e) {
|
|
828
|
+
return "";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
662
832
|
// src/engine/requests.ts
|
|
663
833
|
var ANTHROPIC_MESSAGES_API_URL = "https://api.anthropic.com/v1/messages";
|
|
664
834
|
var ANTHROPIC_VERSION = "2023-06-01";
|
|
@@ -674,7 +844,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
674
844
|
var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
|
|
675
845
|
var MCP_NAME = "BunnyQuery";
|
|
676
846
|
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
|
|
677
|
-
var DEFAULT_OPENAI_MODEL = "gpt-5.
|
|
847
|
+
var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
678
848
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
679
849
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
680
850
|
var getOpenAIImageDetail = (model) => {
|
|
@@ -1193,20 +1363,35 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1193
1363
|
var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
|
|
1194
1364
|
var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
|
|
1195
1365
|
var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
|
|
1366
|
+
var createdTs = Number(item && item.created);
|
|
1367
|
+
var updatedTs = Number(item && item.updated);
|
|
1368
|
+
var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
|
|
1369
|
+
var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
|
|
1196
1370
|
if (userText) {
|
|
1197
1371
|
var displayContent;
|
|
1372
|
+
var indexFile = void 0;
|
|
1198
1373
|
if (item._isBgTask) {
|
|
1199
1374
|
var nameMatch = userText.match(/^- name: (.+)$/m);
|
|
1200
1375
|
if (nameMatch) {
|
|
1201
1376
|
var mimeMatch = userText.match(/^- mime type: (.+)$/m);
|
|
1202
1377
|
var sizeMatch = userText.match(/^- size \(bytes\): (\d+)$/m);
|
|
1203
1378
|
var pathMatch = userText.match(/^- storage path: (.+)$/m);
|
|
1379
|
+
var isContinuePass = userText.indexOf("CONTINUE indexing") === 0;
|
|
1204
1380
|
displayContent = opts.formatIndexingLabel(
|
|
1205
1381
|
nameMatch[1].trim(),
|
|
1206
1382
|
mimeMatch ? mimeMatch[1].trim() : "",
|
|
1207
1383
|
sizeMatch ? Number(sizeMatch[1]) : null,
|
|
1208
|
-
pathMatch ? pathMatch[1].trim() : void 0
|
|
1384
|
+
pathMatch ? pathMatch[1].trim() : void 0,
|
|
1385
|
+
false,
|
|
1386
|
+
isContinuePass
|
|
1209
1387
|
);
|
|
1388
|
+
indexFile = {
|
|
1389
|
+
name: nameMatch[1].trim(),
|
|
1390
|
+
path: pathMatch ? pathMatch[1].trim() : void 0,
|
|
1391
|
+
mime: mimeMatch ? mimeMatch[1].trim() : void 0,
|
|
1392
|
+
size: sizeMatch ? Number(sizeMatch[1]) : void 0,
|
|
1393
|
+
continued: isContinuePass
|
|
1394
|
+
};
|
|
1210
1395
|
} else {
|
|
1211
1396
|
displayContent = userText;
|
|
1212
1397
|
}
|
|
@@ -1218,8 +1403,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1218
1403
|
if (isQueued) userMsg.isPendingQueued = true;
|
|
1219
1404
|
if (isCancelledItem) userMsg.isCancelled = true;
|
|
1220
1405
|
if (item._isBgTask) userMsg.isBackgroundTask = true;
|
|
1406
|
+
if (indexFile) userMsg._indexFile = indexFile;
|
|
1221
1407
|
if (item._isOnBgQueue) userMsg._useBgQueue = true;
|
|
1222
1408
|
if (serverItemId !== void 0) userMsg._serverItemId = serverItemId;
|
|
1409
|
+
if (userTs !== void 0) userMsg._ts = userTs;
|
|
1223
1410
|
mapped.push(userMsg);
|
|
1224
1411
|
}
|
|
1225
1412
|
if (isCancelledItem) ; else if (isInProcess) {
|
|
@@ -1234,17 +1421,104 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1234
1421
|
var em = { role: "assistant", content: getErrorMessage(response), isError: true };
|
|
1235
1422
|
if (item._isBgTask) em.isBackgroundTask = true;
|
|
1236
1423
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
1424
|
+
if (replyTs !== void 0) em._ts = replyTs;
|
|
1237
1425
|
mapped.push(em);
|
|
1238
1426
|
} else if (assistantText) {
|
|
1239
1427
|
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.serviceId, true) };
|
|
1240
1428
|
if (item._isBgTask) okm.isBackgroundTask = true;
|
|
1241
1429
|
if (serverItemId !== void 0) okm._serverItemId = serverItemId;
|
|
1430
|
+
if (replyTs !== void 0) okm._ts = replyTs;
|
|
1242
1431
|
mapped.push(okm);
|
|
1243
1432
|
}
|
|
1244
1433
|
});
|
|
1245
1434
|
return { messages: mapped, runningItemIds };
|
|
1246
1435
|
}
|
|
1247
1436
|
|
|
1437
|
+
// src/engine/viewport_fill.ts
|
|
1438
|
+
var HISTORY_FILL_SLACK_PX = 64;
|
|
1439
|
+
var MAX_HISTORY_FILL_PAGES = 24;
|
|
1440
|
+
var IDLE_WAIT_STEP_MS = 120;
|
|
1441
|
+
var IDLE_WAIT_MAX_MS = 15e3;
|
|
1442
|
+
async function waitForIdle(opts, stale) {
|
|
1443
|
+
var waited = 0;
|
|
1444
|
+
while (opts.isLoading()) {
|
|
1445
|
+
if (stale() || waited >= IDLE_WAIT_MAX_MS) return false;
|
|
1446
|
+
await new Promise(function(r) {
|
|
1447
|
+
setTimeout(r, IDLE_WAIT_STEP_MS);
|
|
1448
|
+
});
|
|
1449
|
+
waited += IDLE_WAIT_STEP_MS;
|
|
1450
|
+
}
|
|
1451
|
+
return !stale();
|
|
1452
|
+
}
|
|
1453
|
+
async function fillHistoryViewport(opts) {
|
|
1454
|
+
var maxPages = typeof opts.maxPages === "number" ? opts.maxPages : MAX_HISTORY_FILL_PAGES;
|
|
1455
|
+
var stale = function() {
|
|
1456
|
+
return !!(opts.isStale && opts.isStale());
|
|
1457
|
+
};
|
|
1458
|
+
var swallowed = 0;
|
|
1459
|
+
for (var page = 0; page < maxPages; page++) {
|
|
1460
|
+
if (stale() || opts.isEndOfList()) return;
|
|
1461
|
+
if (!await waitForIdle(opts, stale)) return;
|
|
1462
|
+
var satisfied = false;
|
|
1463
|
+
try {
|
|
1464
|
+
satisfied = !!await opts.isSatisfied();
|
|
1465
|
+
} catch {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (satisfied || stale()) return;
|
|
1469
|
+
if (!await waitForIdle(opts, stale)) return;
|
|
1470
|
+
var before = opts.messageCount();
|
|
1471
|
+
var attempted;
|
|
1472
|
+
try {
|
|
1473
|
+
attempted = await opts.fetchOlder();
|
|
1474
|
+
} catch {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (stale()) return;
|
|
1478
|
+
if (attempted === false) {
|
|
1479
|
+
if (++swallowed > 3) return;
|
|
1480
|
+
page--;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (opts.messageCount() <= before) return;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function createHistoryFiller(base) {
|
|
1487
|
+
var pending = [];
|
|
1488
|
+
var running = false;
|
|
1489
|
+
async function allSatisfied() {
|
|
1490
|
+
var next = [];
|
|
1491
|
+
for (var i = 0; i < pending.length; i++) {
|
|
1492
|
+
if (!await pending[i]()) next.push(pending[i]);
|
|
1493
|
+
}
|
|
1494
|
+
pending = next;
|
|
1495
|
+
return pending.length === 0;
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
isRunning: function() {
|
|
1499
|
+
return running;
|
|
1500
|
+
},
|
|
1501
|
+
fill: function(isSatisfied) {
|
|
1502
|
+
pending.push(isSatisfied);
|
|
1503
|
+
if (running) return Promise.resolve();
|
|
1504
|
+
running = true;
|
|
1505
|
+
var done = function() {
|
|
1506
|
+
running = false;
|
|
1507
|
+
pending = [];
|
|
1508
|
+
};
|
|
1509
|
+
return fillHistoryViewport({
|
|
1510
|
+
isSatisfied: allSatisfied,
|
|
1511
|
+
isEndOfList: base.isEndOfList,
|
|
1512
|
+
isLoading: base.isLoading,
|
|
1513
|
+
messageCount: base.messageCount,
|
|
1514
|
+
fetchOlder: base.fetchOlder,
|
|
1515
|
+
isStale: base.isStale,
|
|
1516
|
+
maxPages: base.maxPages
|
|
1517
|
+
}).then(done, done);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1248
1522
|
// src/engine/session.ts
|
|
1249
1523
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1250
1524
|
function nowMs() {
|
|
@@ -1282,6 +1556,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1282
1556
|
};
|
|
1283
1557
|
this.bgTaskQueue = [];
|
|
1284
1558
|
this.cancelledServerIds = /* @__PURE__ */ new Set();
|
|
1559
|
+
this.cancelledIndexKeys = /* @__PURE__ */ new Set();
|
|
1285
1560
|
this.pendingAgentRequests = {};
|
|
1286
1561
|
this.aiChatHistoryCache = {};
|
|
1287
1562
|
this.historyItemPolls = /* @__PURE__ */ new Map();
|
|
@@ -1304,6 +1579,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1304
1579
|
this.historyItemPolls.set(id, { kind, stop });
|
|
1305
1580
|
return p;
|
|
1306
1581
|
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Stop and forget one item's poll. Used after a cancel: the row is either gone
|
|
1584
|
+
* (cancelled while queued) or flagged cancelled (cancelled while running), so
|
|
1585
|
+
* asking about it again only burns requests. Safe when no poll is attached, and
|
|
1586
|
+
* safe on an older skapi-js with no stop handle (the entry is then LEFT in the
|
|
1587
|
+
* map so a later drain cannot stack a second, unstoppable poll on the id).
|
|
1588
|
+
*/
|
|
1589
|
+
_stopPoll(id) {
|
|
1590
|
+
var handle = this.historyItemPolls.get(id);
|
|
1591
|
+
if (!handle) return;
|
|
1592
|
+
if (typeof handle.stop !== "function") return;
|
|
1593
|
+
try {
|
|
1594
|
+
handle.stop();
|
|
1595
|
+
} catch (e) {
|
|
1596
|
+
}
|
|
1597
|
+
this.historyItemPolls.delete(id);
|
|
1598
|
+
}
|
|
1307
1599
|
/** True while any pause reason is active. */
|
|
1308
1600
|
isPollingPaused() {
|
|
1309
1601
|
return this._pauseReasons.size > 0;
|
|
@@ -1406,7 +1698,18 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1406
1698
|
if (reply._serverItemId === void 0 && msgs[thIdx]._serverItemId !== void 0) reply._serverItemId = msgs[thIdx]._serverItemId;
|
|
1407
1699
|
msgs[thIdx] = reply;
|
|
1408
1700
|
} else {
|
|
1409
|
-
|
|
1701
|
+
var dupIdx = -1;
|
|
1702
|
+
if (serverId) {
|
|
1703
|
+
for (var d = msgs.length - 1; d >= 0; d--) {
|
|
1704
|
+
var dm = msgs[d];
|
|
1705
|
+
if (dm && dm.role === "assistant" && dm._serverItemId === serverId) {
|
|
1706
|
+
dupIdx = d;
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (dupIdx !== -1) msgs[dupIdx] = reply;
|
|
1712
|
+
else msgs.push(reply);
|
|
1410
1713
|
}
|
|
1411
1714
|
for (var j = 0; j < msgs.length; j++) {
|
|
1412
1715
|
var u = msgs[j];
|
|
@@ -1537,7 +1840,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1537
1840
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
1538
1841
|
this.aiChatHistoryCache[key] = {
|
|
1539
1842
|
messages: offExisting.messages.concat([
|
|
1540
|
-
{ role: "user", content: composed, _ownerKey: key },
|
|
1843
|
+
{ role: "user", content: composed, _ownerKey: key, _ts: wallClockNow() },
|
|
1541
1844
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
1542
1845
|
]),
|
|
1543
1846
|
endOfList: offExisting.endOfList,
|
|
@@ -1569,7 +1872,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1569
1872
|
serviceId: id.serviceId,
|
|
1570
1873
|
history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
|
|
1571
1874
|
});
|
|
1572
|
-
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true };
|
|
1875
|
+
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
1573
1876
|
if (key) queuedBubble._ownerKey = key;
|
|
1574
1877
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
1575
1878
|
this.state.messages.push(queuedBubble);
|
|
@@ -1604,7 +1907,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1604
1907
|
});
|
|
1605
1908
|
return;
|
|
1606
1909
|
}
|
|
1607
|
-
this.state.messages.push({ role: "user", content: composed, ...key ? { _ownerKey: key } : {} });
|
|
1910
|
+
this.state.messages.push({ role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} });
|
|
1608
1911
|
this.state.messages.push({ role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} });
|
|
1609
1912
|
this.host.notify();
|
|
1610
1913
|
this.updateHistoryCache();
|
|
@@ -1646,7 +1949,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1646
1949
|
self.state.sending = false;
|
|
1647
1950
|
if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
|
|
1648
1951
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
1649
|
-
self.host.
|
|
1952
|
+
self.host.scrollToBottomIfSticky(true);
|
|
1650
1953
|
});
|
|
1651
1954
|
});
|
|
1652
1955
|
}
|
|
@@ -1660,6 +1963,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1660
1963
|
if (nextIdx === -1) return;
|
|
1661
1964
|
var existing = this.state.messages[nextIdx];
|
|
1662
1965
|
var promoted = { role: "user", content: existing.content, isPendingInProcess: true, isBackgroundTask: true };
|
|
1966
|
+
if (existing._indexFile) promoted._indexFile = existing._indexFile;
|
|
1967
|
+
if (existing._ts !== void 0) promoted._ts = existing._ts;
|
|
1663
1968
|
if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
|
|
1664
1969
|
if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
|
|
1665
1970
|
this.state.messages[nextIdx] = promoted;
|
|
@@ -1680,6 +1985,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1680
1985
|
var existing = this.state.messages[nextIdx];
|
|
1681
1986
|
var promoted = { role: "user", content: existing.content, isPendingInProcess: true };
|
|
1682
1987
|
if (existing.isBackgroundTask) promoted.isBackgroundTask = true;
|
|
1988
|
+
if (existing._indexFile) promoted._indexFile = existing._indexFile;
|
|
1989
|
+
if (existing._ts !== void 0) promoted._ts = existing._ts;
|
|
1683
1990
|
if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
|
|
1684
1991
|
if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
|
|
1685
1992
|
if (existing.isSendingToServer) promoted.isSendingToServer = true;
|
|
@@ -1729,6 +2036,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1729
2036
|
var repl = { role: "user", content: exist.content };
|
|
1730
2037
|
if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
|
|
1731
2038
|
if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
|
|
2039
|
+
if (exist._ts !== void 0) repl._ts = exist._ts;
|
|
1732
2040
|
this.state.messages[userIdx] = repl;
|
|
1733
2041
|
}
|
|
1734
2042
|
var thinkingIdx = userIdx >= 0 ? this.state.messages.findIndex(function(m, i) {
|
|
@@ -1737,6 +2045,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1737
2045
|
return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
|
|
1738
2046
|
}
|
|
1739
2047
|
insertAtTarget(msg, targetIdx) {
|
|
2048
|
+
if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
|
|
1740
2049
|
if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) this.state.messages[targetIdx] = msg;
|
|
1741
2050
|
else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
|
|
1742
2051
|
else this.state.messages.push(msg);
|
|
@@ -1780,7 +2089,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1780
2089
|
this.promoteNextQueuedToRunning();
|
|
1781
2090
|
this.updateHistoryCache();
|
|
1782
2091
|
this.host.notify();
|
|
1783
|
-
this.host.
|
|
2092
|
+
this.host.scrollToBottomIfSticky(true);
|
|
1784
2093
|
}
|
|
1785
2094
|
onQueuedSendError(_composed, err, serverId, ownerKey) {
|
|
1786
2095
|
if (serverId) this.historyItemPolls.delete(serverId);
|
|
@@ -1830,7 +2139,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1830
2139
|
this.promoteNextQueuedToRunning();
|
|
1831
2140
|
this.updateHistoryCache();
|
|
1832
2141
|
this.host.notify();
|
|
1833
|
-
this.host.
|
|
2142
|
+
this.host.scrollToBottomIfSticky(true);
|
|
1834
2143
|
return;
|
|
1835
2144
|
}
|
|
1836
2145
|
var targetIdx = this.resolveQueuedUserBubble(serverId);
|
|
@@ -1844,7 +2153,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1844
2153
|
this.promoteNextQueuedToRunning();
|
|
1845
2154
|
this.updateHistoryCache();
|
|
1846
2155
|
this.host.notify();
|
|
1847
|
-
this.host.
|
|
2156
|
+
this.host.scrollToBottomIfSticky(true);
|
|
1848
2157
|
}
|
|
1849
2158
|
cancelQueuedMessage(msg, idx) {
|
|
1850
2159
|
var self = this;
|
|
@@ -1868,6 +2177,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1868
2177
|
})).then(function(result) {
|
|
1869
2178
|
if (result && result.removed) {
|
|
1870
2179
|
self.cancelledServerIds.add(serverId);
|
|
2180
|
+
self._stopPoll(serverId);
|
|
1871
2181
|
var qi = self.bgTaskQueue.findIndex(function(e) {
|
|
1872
2182
|
return e.id === serverId;
|
|
1873
2183
|
});
|
|
@@ -1876,7 +2186,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1876
2186
|
return m._serverItemId === serverId && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user";
|
|
1877
2187
|
});
|
|
1878
2188
|
if (removeIdx !== -1) {
|
|
1879
|
-
|
|
2189
|
+
var wasMsg = self.state.messages[removeIdx];
|
|
2190
|
+
var cancelledMsg = { role: "user", content: wasMsg.content, isCancelled: true, _serverItemId: serverId };
|
|
2191
|
+
if (wasMsg.isBackgroundTask) cancelledMsg.isBackgroundTask = true;
|
|
2192
|
+
if (wasMsg._indexFile) cancelledMsg._indexFile = wasMsg._indexFile;
|
|
2193
|
+
if (wasMsg._useBgQueue) cancelledMsg._useBgQueue = true;
|
|
2194
|
+
if (wasMsg._ownerKey !== void 0) cancelledMsg._ownerKey = wasMsg._ownerKey;
|
|
2195
|
+
self.state.messages[removeIdx] = cancelledMsg;
|
|
1880
2196
|
var thById = self.state.messages.findIndex(function(m) {
|
|
1881
2197
|
return m._serverItemId === serverId && m.isPending && m.role === "assistant";
|
|
1882
2198
|
});
|
|
@@ -1913,6 +2229,42 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1913
2229
|
}
|
|
1914
2230
|
});
|
|
1915
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Stop indexing a file, from its collapsed row — every pass at once, not just
|
|
2234
|
+
* the bubble the user happens to see.
|
|
2235
|
+
*
|
|
2236
|
+
* A big file is indexed as a CHAIN of passes, so cancelling only the live one
|
|
2237
|
+
* accomplishes nothing: the next pass is dispatched as soon as it settles.
|
|
2238
|
+
* Three things end the chain:
|
|
2239
|
+
* 1. every queued/running pass of this file is cancelled server-side
|
|
2240
|
+
* (csr-cancel deletes a queued row and flags a running one "cancelled",
|
|
2241
|
+
* which is also the worker's gate for NOT enqueueing the next window);
|
|
2242
|
+
* 2. the file is remembered in cancelledIndexKeys, so the client-driven
|
|
2243
|
+
* resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
|
|
2244
|
+
* 3. any of its passes still sitting in bgTaskQueue is dropped by the next
|
|
2245
|
+
* drain rather than surfacing a fresh "Indexing…" bubble.
|
|
2246
|
+
*
|
|
2247
|
+
* Records already written by the passes that DID run are kept — this stops the
|
|
2248
|
+
* work, it does not undo it.
|
|
2249
|
+
*/
|
|
2250
|
+
cancelIndexingGroup(group) {
|
|
2251
|
+
var self = this;
|
|
2252
|
+
if (!group || !group.key) return;
|
|
2253
|
+
var scoped = this.getHistoryCacheKey() + "|" + group.key;
|
|
2254
|
+
this.cancelledIndexKeys.add(scoped);
|
|
2255
|
+
var ids = group.cancellableIds || [];
|
|
2256
|
+
if (!ids.length) {
|
|
2257
|
+
this.host.notify();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
ids.forEach(function(serverId) {
|
|
2261
|
+
var idx = self.state.messages.findIndex(function(m) {
|
|
2262
|
+
return m._serverItemId === serverId && m.role === "user" && (m.isPendingQueued || m.isPendingInProcess);
|
|
2263
|
+
});
|
|
2264
|
+
if (idx === -1) return;
|
|
2265
|
+
self.cancelQueuedMessage(self.state.messages[idx], idx);
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
1916
2268
|
// --- typewriter -------------------------------------------------------
|
|
1917
2269
|
// Reveal `fullText` into a message bubble at a constant wall-clock RATE
|
|
1918
2270
|
// (chars/second) driven by requestAnimationFrame, rather than a fixed number
|
|
@@ -2030,6 +2382,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2030
2382
|
}
|
|
2031
2383
|
enqueueTypewrite(idx, fullText, localId) {
|
|
2032
2384
|
var self = this;
|
|
2385
|
+
var target = this.state.messages[idx];
|
|
2386
|
+
if (target && target._ts === void 0) target._ts = wallClockNow();
|
|
2033
2387
|
this.typewriterQueue = this.typewriterQueue.then(function() {
|
|
2034
2388
|
return self.typewriteIntoIndex(idx, fullText, localId);
|
|
2035
2389
|
});
|
|
@@ -2099,6 +2453,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2099
2453
|
var u = this.state.messages[uIdx];
|
|
2100
2454
|
var cleaned = { role: "user", content: u.content, _serverItemId: itemId };
|
|
2101
2455
|
if (u.isBackgroundTask) cleaned.isBackgroundTask = true;
|
|
2456
|
+
if (u._ts !== void 0) cleaned._ts = u._ts;
|
|
2457
|
+
if (u._indexFile) cleaned._indexFile = u._indexFile;
|
|
2102
2458
|
this.state.messages[uIdx] = cleaned;
|
|
2103
2459
|
}
|
|
2104
2460
|
// If an immediate-send request for the current cache key is still in flight
|
|
@@ -2116,13 +2472,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2116
2472
|
return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
|
|
2117
2473
|
})) return Promise.resolve();
|
|
2118
2474
|
this.state.sending = true;
|
|
2119
|
-
this.host.
|
|
2475
|
+
this.host.scrollToBottomIfSticky(true);
|
|
2120
2476
|
return Promise.resolve(pending).catch(function() {
|
|
2121
2477
|
}).then(function() {
|
|
2122
2478
|
if (token !== self.state.gateRefreshToken) return;
|
|
2123
2479
|
self.state.sending = false;
|
|
2124
2480
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
2125
|
-
self.host.
|
|
2481
|
+
self.host.scrollToBottomIfSticky(true);
|
|
2126
2482
|
});
|
|
2127
2483
|
});
|
|
2128
2484
|
}
|
|
@@ -2141,8 +2497,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2141
2497
|
});
|
|
2142
2498
|
if (idx !== -1) {
|
|
2143
2499
|
this._clearPendingUserBubble(itemId);
|
|
2500
|
+
var wasBgTask = !!this.state.messages[idx].isBackgroundTask;
|
|
2144
2501
|
if (isErr) {
|
|
2145
2502
|
this.state.messages[idx] = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
|
|
2503
|
+
if (wasBgTask) this.state.messages[idx].isBackgroundTask = true;
|
|
2504
|
+
this.host.notify();
|
|
2505
|
+
this.updateHistoryCache();
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
var text = answer || "No text response received from AI provider.";
|
|
2509
|
+
if (wasBgTask) {
|
|
2510
|
+
this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
|
|
2146
2511
|
this.host.notify();
|
|
2147
2512
|
this.updateHistoryCache();
|
|
2148
2513
|
return;
|
|
@@ -2150,7 +2515,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2150
2515
|
var lid = this._newLocalId();
|
|
2151
2516
|
this.state.messages[idx] = { role: "assistant", content: "", _localId: lid, _serverItemId: itemId };
|
|
2152
2517
|
this.host.notify();
|
|
2153
|
-
this.enqueueTypewrite(idx,
|
|
2518
|
+
this.enqueueTypewrite(idx, text, lid);
|
|
2154
2519
|
this.updateHistoryCache();
|
|
2155
2520
|
return;
|
|
2156
2521
|
}
|
|
@@ -2159,25 +2524,125 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2159
2524
|
});
|
|
2160
2525
|
if (userIdx === -1) return;
|
|
2161
2526
|
var ex = this.state.messages[userIdx];
|
|
2162
|
-
|
|
2527
|
+
var settledUser = { role: "user", content: ex.content, _serverItemId: itemId };
|
|
2528
|
+
if (ex.isBackgroundTask) settledUser.isBackgroundTask = true;
|
|
2529
|
+
if (ex._ts !== void 0) settledUser._ts = ex._ts;
|
|
2530
|
+
if (ex._indexFile) settledUser._indexFile = ex._indexFile;
|
|
2531
|
+
if (ex._useBgQueue) settledUser._useBgQueue = true;
|
|
2532
|
+
this.state.messages[userIdx] = settledUser;
|
|
2163
2533
|
if (isErr) {
|
|
2164
|
-
|
|
2534
|
+
var errReply = { role: "assistant", content: answer, isError: true, _serverItemId: itemId };
|
|
2535
|
+
if (ex.isBackgroundTask) errReply.isBackgroundTask = true;
|
|
2536
|
+
this.state.messages.splice(userIdx + 1, 0, errReply);
|
|
2537
|
+
this.host.notify();
|
|
2538
|
+
this.updateHistoryCache();
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
var text2 = answer || "No text response received from AI provider.";
|
|
2542
|
+
if (ex.isBackgroundTask) {
|
|
2543
|
+
this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
|
|
2165
2544
|
this.host.notify();
|
|
2166
2545
|
this.updateHistoryCache();
|
|
2167
2546
|
return;
|
|
2168
2547
|
}
|
|
2169
2548
|
var lid2 = this._newLocalId();
|
|
2170
|
-
|
|
2549
|
+
var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
|
|
2550
|
+
this.state.messages.splice(userIdx + 1, 0, reply);
|
|
2171
2551
|
this.host.notify();
|
|
2172
|
-
this.enqueueTypewrite(userIdx + 1,
|
|
2552
|
+
this.enqueueTypewrite(userIdx + 1, text2, lid2);
|
|
2173
2553
|
this.updateHistoryCache();
|
|
2174
2554
|
}
|
|
2555
|
+
/** How a bg task maps onto a collapsed row: the row's own key (storage path
|
|
2556
|
+
* when known, else the filename), scoped to the chat it belongs to. A storage
|
|
2557
|
+
* path is project-relative ("report.xlsx"), and ONE ChatSession serves every
|
|
2558
|
+
* project — unscoped, stopping a file in one project would silently suppress
|
|
2559
|
+
* the same filename's continuations in another. */
|
|
2560
|
+
_indexKeyOf(entry) {
|
|
2561
|
+
if (!entry) return "";
|
|
2562
|
+
var file = entry.storagePath || entry.filename;
|
|
2563
|
+
if (!file) return "";
|
|
2564
|
+
return entry.serviceId + "#" + entry.platform + "|" + file;
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Reconcile the bg queue with the files the user has stopped.
|
|
2568
|
+
*
|
|
2569
|
+
* A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
|
|
2570
|
+
* Reindex from the file manager — so it LIFTS the stop: the key is a storage
|
|
2571
|
+
* path, and without this an earlier cancel would silently kill every future
|
|
2572
|
+
* index of the same path. A continuation of a stopped file is dropped instead,
|
|
2573
|
+
* covering the pass that was dispatched in the moment before the cancel landed.
|
|
2574
|
+
*/
|
|
2575
|
+
_applyIndexCancellations() {
|
|
2576
|
+
if (!this.cancelledIndexKeys.size) return;
|
|
2577
|
+
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
2578
|
+
var entry = this.bgTaskQueue[i];
|
|
2579
|
+
var key = this._indexKeyOf(entry);
|
|
2580
|
+
if (!key || !this.cancelledIndexKeys.has(key)) continue;
|
|
2581
|
+
if (!entry.resumePass) {
|
|
2582
|
+
this.cancelledIndexKeys.delete(key);
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
this.bgTaskQueue.splice(i, 1);
|
|
2586
|
+
this._stopPoll(entry.id);
|
|
2587
|
+
this._cancelServerItem(entry.id);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Cancel any live pass of a stopped file that turned up on its own.
|
|
2592
|
+
*
|
|
2593
|
+
* The client is not the only thing that continues a file: for PDFs and (when
|
|
2594
|
+
* windowed indexing is on) text/grid files the WORKER enqueues the next window
|
|
2595
|
+
* itself, and that pass reaches the chat through the history poll, never
|
|
2596
|
+
* through bgTaskQueue. The worker's own gate stops the chain when the running
|
|
2597
|
+
* row is cancelled — but if the row had already finished when the user hit
|
|
2598
|
+
* stop, the next window was queued a moment earlier and still arrives. Stop it
|
|
2599
|
+
* here rather than making the user hit stop again.
|
|
2600
|
+
*
|
|
2601
|
+
* Runs from drainBgTaskQueue, which both clients call after a history load.
|
|
2602
|
+
*/
|
|
2603
|
+
_sweepCancelledIndexing() {
|
|
2604
|
+
if (!this.cancelledIndexKeys.size) return;
|
|
2605
|
+
var self = this;
|
|
2606
|
+
var chatKey = this.getHistoryCacheKey();
|
|
2607
|
+
var targets = [];
|
|
2608
|
+
this.state.messages.forEach(function(m, i) {
|
|
2609
|
+
if (!m.isBackgroundTask || m.role !== "user" || !m._serverItemId) return;
|
|
2610
|
+
if (m._cancelling || m.isSendingToServer) return;
|
|
2611
|
+
if (!(m.isPendingQueued || m.isPendingInProcess)) return;
|
|
2612
|
+
var ref = m._indexFile;
|
|
2613
|
+
var file = ref && (ref.path || ref.name);
|
|
2614
|
+
if (!file || !self.cancelledIndexKeys.has(chatKey + "|" + file)) return;
|
|
2615
|
+
targets.push({ msg: m, idx: i });
|
|
2616
|
+
});
|
|
2617
|
+
targets.forEach(function(t) {
|
|
2618
|
+
var idx = self.state.messages.indexOf(t.msg);
|
|
2619
|
+
self.cancelQueuedMessage(t.msg, idx === -1 ? t.idx : idx);
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
/** Best-effort server-side cancel of a bg-queue item that has no bubble (so
|
|
2623
|
+
* cancelQueuedMessage, which drives one, has nothing to act on). */
|
|
2624
|
+
_cancelServerItem(serverId) {
|
|
2625
|
+
var id = this.host.getIdentity();
|
|
2626
|
+
if (!serverId || id.platform !== "claude" && id.platform !== "openai") return;
|
|
2627
|
+
var url = id.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2628
|
+
Promise.resolve(this.host.cancelRequest({
|
|
2629
|
+
url,
|
|
2630
|
+
method: "POST",
|
|
2631
|
+
id: serverId,
|
|
2632
|
+
queue: (id.userId || id.serviceId) + BG_INDEXING_QUEUE_SUFFIX,
|
|
2633
|
+
service: id.serviceId,
|
|
2634
|
+
owner: id.owner
|
|
2635
|
+
})).catch(function() {
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2175
2638
|
// Inject "Indexing: <file>" bubbles for queued bg tasks + attach their polls.
|
|
2176
2639
|
drainBgTaskQueue() {
|
|
2177
2640
|
var self = this;
|
|
2178
2641
|
var id = this.host.getIdentity();
|
|
2179
2642
|
var svcId = id.serviceId, plat = id.platform;
|
|
2180
2643
|
if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
|
|
2644
|
+
this._applyIndexCancellations();
|
|
2645
|
+
this._sweepCancelledIndexing();
|
|
2181
2646
|
var presentIds = {};
|
|
2182
2647
|
var pendingIds = {};
|
|
2183
2648
|
this.state.messages.forEach(function(m) {
|
|
@@ -2195,7 +2660,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2195
2660
|
if (entry.serviceId !== svcId || entry.platform !== plat) return;
|
|
2196
2661
|
if (!presentIds[entry.id]) {
|
|
2197
2662
|
var isRunning = entry.status === "running";
|
|
2198
|
-
var userBubble = {
|
|
2663
|
+
var userBubble = {
|
|
2664
|
+
role: "user",
|
|
2665
|
+
content: self.host.formatIndexingLabel(entry.filename, entry.mime, entry.size, entry.storagePath, entry.isReindex, !!entry.resumePass),
|
|
2666
|
+
isBackgroundTask: true,
|
|
2667
|
+
_serverItemId: entry.id,
|
|
2668
|
+
// Structured ref so this live pass groups with the same file's passes
|
|
2669
|
+
// rebuilt from history (see indexing_groups.buildChatDisplayList).
|
|
2670
|
+
_indexFile: {
|
|
2671
|
+
name: entry.filename,
|
|
2672
|
+
path: entry.storagePath,
|
|
2673
|
+
mime: entry.mime,
|
|
2674
|
+
size: entry.size,
|
|
2675
|
+
isReindex: !!entry.isReindex,
|
|
2676
|
+
continued: !!entry.resumePass
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2199
2679
|
if (isRunning) userBubble.isPendingInProcess = true;
|
|
2200
2680
|
else userBubble.isPendingQueued = true;
|
|
2201
2681
|
self.state.messages.push(userBubble);
|
|
@@ -2205,7 +2685,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2205
2685
|
presentIds[entry.id] = true;
|
|
2206
2686
|
self.host.notify();
|
|
2207
2687
|
self.updateHistoryCache();
|
|
2208
|
-
self.host.
|
|
2688
|
+
self.host.scrollToBottomIfSticky(false);
|
|
2209
2689
|
}
|
|
2210
2690
|
if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
2211
2691
|
var capturedId = entry.id, capturedPlat = plat;
|
|
@@ -2223,15 +2703,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2223
2703
|
}).catch(function(err) {
|
|
2224
2704
|
self.historyItemPolls.delete(capturedId);
|
|
2225
2705
|
var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
2706
|
+
self._clearPendingUserBubble(capturedId);
|
|
2226
2707
|
var bi = self.state.messages.findIndex(function(m) {
|
|
2227
2708
|
return m.isPending && m._serverItemId === capturedId;
|
|
2228
2709
|
});
|
|
2229
2710
|
if (bi !== -1) {
|
|
2230
2711
|
if (isNotExists) self.state.messages.splice(bi, 1);
|
|
2231
2712
|
else self.state.messages[bi] = { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId };
|
|
2232
|
-
|
|
2233
|
-
self.
|
|
2713
|
+
} else if (!isNotExists) {
|
|
2714
|
+
var ui = self.state.messages.findIndex(function(m) {
|
|
2715
|
+
return m.role === "user" && m._serverItemId === capturedId;
|
|
2716
|
+
});
|
|
2717
|
+
if (ui !== -1) {
|
|
2718
|
+
self.state.messages.splice(ui + 1, 0, { role: "assistant", content: getErrorMessage(err), isError: true, isBackgroundTask: true, _serverItemId: capturedId });
|
|
2719
|
+
}
|
|
2234
2720
|
}
|
|
2721
|
+
self.host.notify();
|
|
2722
|
+
self.updateHistoryCache();
|
|
2235
2723
|
}).then(function() {
|
|
2236
2724
|
if (wasStopped) return;
|
|
2237
2725
|
var qi = self.bgTaskQueue.findIndex(function(q) {
|
|
@@ -2259,8 +2747,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2259
2747
|
var self = this;
|
|
2260
2748
|
try {
|
|
2261
2749
|
if (!entry || !entry.storagePath) return;
|
|
2750
|
+
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
|
|
2262
2751
|
if (!isPagedReadFile(entry.filename, entry.mime)) return;
|
|
2263
2752
|
if (isImageVisionFile(entry.filename, entry.mime)) return;
|
|
2753
|
+
if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
|
|
2264
2754
|
if (isErrorResponseBody(response)) return;
|
|
2265
2755
|
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
2266
2756
|
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
|
|
@@ -2353,6 +2843,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2353
2843
|
serviceId: id.serviceId,
|
|
2354
2844
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
2355
2845
|
}).messages;
|
|
2846
|
+
var keptOlderPages = false;
|
|
2356
2847
|
if (fetchMore) {
|
|
2357
2848
|
self.state.messages = mapped.concat(self.state.messages);
|
|
2358
2849
|
} else {
|
|
@@ -2363,7 +2854,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2363
2854
|
});
|
|
2364
2855
|
var locallyCancelled = {};
|
|
2365
2856
|
self.state.messages.forEach(function(m) {
|
|
2366
|
-
if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] =
|
|
2857
|
+
if (m.isCancelled && m._serverItemId) locallyCancelled[m._serverItemId] = m;
|
|
2858
|
+
});
|
|
2859
|
+
var inFlightCancel = {};
|
|
2860
|
+
self.state.messages.forEach(function(m) {
|
|
2861
|
+
if (!m._serverItemId) return;
|
|
2862
|
+
if (m._cancelling || m._cancelError) inFlightCancel[m._serverItemId] = m;
|
|
2367
2863
|
});
|
|
2368
2864
|
var mappedHasPendingAssistant = mapped.some(function(m) {
|
|
2369
2865
|
return m.isPending && m.role === "assistant" && !m.isBackgroundTask;
|
|
@@ -2383,7 +2879,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2383
2879
|
}
|
|
2384
2880
|
}
|
|
2385
2881
|
}
|
|
2386
|
-
|
|
2882
|
+
var oldestInPage1 = void 0;
|
|
2883
|
+
mapped.forEach(function(m) {
|
|
2884
|
+
var sid = m._serverItemId;
|
|
2885
|
+
if (typeof sid !== "string") return;
|
|
2886
|
+
if (oldestInPage1 === void 0 || sid < oldestInPage1) oldestInPage1 = sid;
|
|
2887
|
+
});
|
|
2888
|
+
var sharesPage1 = self.state.messages.some(function(m) {
|
|
2889
|
+
return typeof m._serverItemId === "string" && !!serverIds[m._serverItemId];
|
|
2890
|
+
});
|
|
2891
|
+
var retainedOlder = !sharesPage1 || oldestInPage1 === void 0 ? [] : self.state.messages.filter(function(m) {
|
|
2892
|
+
if (typeof m._serverItemId !== "string") return false;
|
|
2893
|
+
if (m._ownerKey !== void 0 && m._ownerKey !== loadKey) return false;
|
|
2894
|
+
return m._serverItemId < oldestInPage1;
|
|
2895
|
+
});
|
|
2896
|
+
keptOlderPages = retainedOlder.length > 0;
|
|
2897
|
+
self.state.messages = keptOlderPages ? retainedOlder.concat(mapped) : mapped;
|
|
2387
2898
|
rescued.forEach(function(m) {
|
|
2388
2899
|
self.state.messages.push(m);
|
|
2389
2900
|
});
|
|
@@ -2391,19 +2902,38 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2391
2902
|
for (var ci = 0; ci < self.state.messages.length; ci++) {
|
|
2392
2903
|
var c = self.state.messages[ci];
|
|
2393
2904
|
if (!c._serverItemId || !locallyCancelled[c._serverItemId] || c.isCancelled) continue;
|
|
2394
|
-
self.state.messages[ci] = {
|
|
2905
|
+
self.state.messages[ci] = {
|
|
2906
|
+
role: "user",
|
|
2907
|
+
content: c.content,
|
|
2908
|
+
isCancelled: true,
|
|
2909
|
+
_serverItemId: c._serverItemId,
|
|
2910
|
+
isBackgroundTask: c.isBackgroundTask,
|
|
2911
|
+
_indexFile: c._indexFile,
|
|
2912
|
+
_useBgQueue: c._useBgQueue,
|
|
2913
|
+
_ownerKey: c._ownerKey
|
|
2914
|
+
};
|
|
2395
2915
|
if (ci + 1 < self.state.messages.length && self.state.messages[ci + 1].isPending && self.state.messages[ci + 1]._serverItemId === c._serverItemId) {
|
|
2396
2916
|
self.state.messages.splice(ci + 1, 1);
|
|
2397
2917
|
}
|
|
2398
2918
|
}
|
|
2399
2919
|
}
|
|
2920
|
+
for (var fi = 0; fi < self.state.messages.length; fi++) {
|
|
2921
|
+
var fm = self.state.messages[fi];
|
|
2922
|
+
var was = fm._serverItemId && inFlightCancel[fm._serverItemId];
|
|
2923
|
+
if (!was || fm.isCancelled) continue;
|
|
2924
|
+
if (!(fm.isPendingQueued || fm.isPendingInProcess || fm.isPending)) continue;
|
|
2925
|
+
if (was._cancelling) fm._cancelling = true;
|
|
2926
|
+
if (was._cancelError) fm._cancelError = was._cancelError;
|
|
2927
|
+
}
|
|
2400
2928
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2929
|
+
if (!keptOlderPages) {
|
|
2930
|
+
self.state.historyEndOfList = !!(history && history.endOfList);
|
|
2931
|
+
self.state.historyStartKeyHistory = history && Array.isArray(history.startKeyHistory) ? history.startKeyHistory : [];
|
|
2932
|
+
var clearedAt = self.host.getClearedAt();
|
|
2933
|
+
if (clearedAt && chatList.length > 0) {
|
|
2934
|
+
var oldestUpdated = Number(chatList[chatList.length - 1] && chatList[chatList.length - 1].updated);
|
|
2935
|
+
if (isFinite(oldestUpdated) && oldestUpdated <= clearedAt) self.state.historyEndOfList = true;
|
|
2936
|
+
}
|
|
2407
2937
|
}
|
|
2408
2938
|
if (self.state.historyRequestToken === token) {
|
|
2409
2939
|
self.state.loadingHistory = false;
|
|
@@ -2432,18 +2962,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2432
2962
|
return m.isPending && m._serverItemId === capturedId;
|
|
2433
2963
|
});
|
|
2434
2964
|
if (isNotExists) {
|
|
2435
|
-
var
|
|
2965
|
+
var uIdx = self.state.messages.findIndex(function(m) {
|
|
2966
|
+
return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
|
|
2967
|
+
});
|
|
2968
|
+
var isBg = aIdx !== -1 && !!self.state.messages[aIdx].isBackgroundTask || uIdx !== -1 && !!self.state.messages[uIdx].isBackgroundTask;
|
|
2436
2969
|
if (aIdx !== -1) self.state.messages.splice(aIdx, 1);
|
|
2437
2970
|
if (!isBg) {
|
|
2438
|
-
var uIdx = self.state.messages.findIndex(function(m) {
|
|
2439
|
-
return m.role === "user" && m._serverItemId === capturedId && !m.isCancelled;
|
|
2440
|
-
});
|
|
2441
2971
|
if (uIdx !== -1) {
|
|
2442
2972
|
var ex = self.state.messages[uIdx];
|
|
2443
2973
|
self.state.messages[uIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId };
|
|
2444
2974
|
}
|
|
2445
2975
|
self.cancelledServerIds.delete(capturedId);
|
|
2446
2976
|
self.promoteNextQueuedToRunning();
|
|
2977
|
+
} else if (uIdx !== -1) {
|
|
2978
|
+
var bex = self.state.messages[uIdx];
|
|
2979
|
+
var bcancelled = { role: "user", content: bex.content, isCancelled: true, isBackgroundTask: true, _serverItemId: bex._serverItemId };
|
|
2980
|
+
if (bex._indexFile) bcancelled._indexFile = bex._indexFile;
|
|
2981
|
+
if (bex._useBgQueue) bcancelled._useBgQueue = true;
|
|
2982
|
+
self.state.messages[uIdx] = bcancelled;
|
|
2983
|
+
self.promoteNextBgQueuedToRunning();
|
|
2447
2984
|
}
|
|
2448
2985
|
self.host.notify();
|
|
2449
2986
|
self.updateHistoryCache();
|
|
@@ -2464,7 +3001,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2464
3001
|
});
|
|
2465
3002
|
self.drainBgTaskQueue();
|
|
2466
3003
|
}
|
|
2467
|
-
if (!fetchMore) return self.host.
|
|
3004
|
+
if (!fetchMore) return self.host.scrollToBottomIfSticky();
|
|
2468
3005
|
}).catch(function(err) {
|
|
2469
3006
|
console.warn("[chat-engine] getChatHistory failed", err);
|
|
2470
3007
|
}).then(function() {
|
|
@@ -2474,6 +3011,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2474
3011
|
self.state.loadingOlderHistory = false;
|
|
2475
3012
|
if (wasLoading) self.host.notify();
|
|
2476
3013
|
}
|
|
3014
|
+
if (self.host.onHistoryLoaded) self.host.onHistoryLoaded(!!fetchMore, token);
|
|
2477
3015
|
});
|
|
2478
3016
|
}
|
|
2479
3017
|
// --- attachment upload orchestration ---------------------------------
|
|
@@ -2671,11 +3209,199 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2671
3209
|
}
|
|
2672
3210
|
};
|
|
2673
3211
|
|
|
3212
|
+
// src/engine/indexing_groups.ts
|
|
3213
|
+
var INDEXING_LABEL_RE = /^(Re)?[Ii]ndexing(\s*\(continuing\))?\s*:?\s+(.+)$/;
|
|
3214
|
+
var LEADING_MD_LINK_RE = /^\[([^\]]+)\]\(([^)]+)\)/;
|
|
3215
|
+
function parseIndexingLabel(content) {
|
|
3216
|
+
if (typeof content !== "string" || !content) return null;
|
|
3217
|
+
var firstLine = content.split("\n")[0].trim();
|
|
3218
|
+
var m = firstLine.match(INDEXING_LABEL_RE);
|
|
3219
|
+
if (!m) return null;
|
|
3220
|
+
var head = m[3].split(" \xB7 ")[0].trim();
|
|
3221
|
+
var link = head.match(LEADING_MD_LINK_RE);
|
|
3222
|
+
var name = link ? link[1].trim() : head;
|
|
3223
|
+
if (!name) return null;
|
|
3224
|
+
return {
|
|
3225
|
+
name,
|
|
3226
|
+
path: link ? link[2].trim() : void 0,
|
|
3227
|
+
continued: !!m[2],
|
|
3228
|
+
isReindex: !!m[1]
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
function readFileRef(msg) {
|
|
3232
|
+
var ref = msg && msg._indexFile;
|
|
3233
|
+
if (ref && (ref.path || ref.name)) {
|
|
3234
|
+
return {
|
|
3235
|
+
name: ref.name || ref.path || "",
|
|
3236
|
+
path: ref.path,
|
|
3237
|
+
mime: ref.mime,
|
|
3238
|
+
size: ref.size,
|
|
3239
|
+
isReindex: ref.isReindex,
|
|
3240
|
+
continued: !!ref.continued
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
var parsed = parseIndexingLabel(msg && msg.content);
|
|
3244
|
+
if (!parsed) return null;
|
|
3245
|
+
return {
|
|
3246
|
+
name: parsed.name,
|
|
3247
|
+
path: parsed.path,
|
|
3248
|
+
isReindex: parsed.isReindex,
|
|
3249
|
+
continued: parsed.continued
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
function isPendingMsg(m) {
|
|
3253
|
+
return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
|
|
3254
|
+
}
|
|
3255
|
+
function buildChatDisplayList(messages, opts) {
|
|
3256
|
+
var list = Array.isArray(messages) ? messages : [];
|
|
3257
|
+
var hasMoreHistory = !!(opts && opts.hasMoreHistory);
|
|
3258
|
+
var groups = {};
|
|
3259
|
+
var order = [];
|
|
3260
|
+
var runOfIndex = new Array(list.length);
|
|
3261
|
+
var runByItemId = {};
|
|
3262
|
+
var keyByName = {};
|
|
3263
|
+
var openRunOfKey = {};
|
|
3264
|
+
var runsOfKey = {};
|
|
3265
|
+
var keyOfRun = {};
|
|
3266
|
+
var runSeq = 0;
|
|
3267
|
+
for (var i = 0; i < list.length; i++) {
|
|
3268
|
+
var msg = list[i];
|
|
3269
|
+
if (!msg || !msg.isBackgroundTask) continue;
|
|
3270
|
+
var runId;
|
|
3271
|
+
var ref = msg.role === "user" ? readFileRef(msg) : null;
|
|
3272
|
+
if (ref) {
|
|
3273
|
+
var key = ref.path || keyByName[ref.name] || ref.name;
|
|
3274
|
+
if (!ref.continued && openRunOfKey[key]) delete openRunOfKey[key];
|
|
3275
|
+
runId = openRunOfKey[key];
|
|
3276
|
+
if (!runId) {
|
|
3277
|
+
runId = "run" + runSeq++;
|
|
3278
|
+
openRunOfKey[key] = runId;
|
|
3279
|
+
keyOfRun[runId] = key;
|
|
3280
|
+
(runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
|
|
3281
|
+
}
|
|
3282
|
+
} else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
|
|
3283
|
+
runId = runByItemId[msg._serverItemId];
|
|
3284
|
+
} else if (msg.role !== "user") {
|
|
3285
|
+
runId = runOfIndex[i - 1];
|
|
3286
|
+
}
|
|
3287
|
+
if (!runId) continue;
|
|
3288
|
+
var g = groups[runId];
|
|
3289
|
+
if (!g) {
|
|
3290
|
+
var fileKey = keyOfRun[runId];
|
|
3291
|
+
g = groups[runId] = {
|
|
3292
|
+
key: fileKey,
|
|
3293
|
+
runKey: runId,
|
|
3294
|
+
// provisional; renumbered newest-first below
|
|
3295
|
+
name: ref ? ref.name : fileKey,
|
|
3296
|
+
path: ref ? ref.path : void 0,
|
|
3297
|
+
mime: ref ? ref.mime : void 0,
|
|
3298
|
+
size: ref ? ref.size : void 0,
|
|
3299
|
+
isReindex: !!(ref && ref.isReindex),
|
|
3300
|
+
members: [],
|
|
3301
|
+
passCount: 0,
|
|
3302
|
+
status: "done",
|
|
3303
|
+
cancellableIds: [],
|
|
3304
|
+
cancelling: false,
|
|
3305
|
+
mayHaveOlder: false,
|
|
3306
|
+
// The run's first loaded pass, and never re-stamped: see the file
|
|
3307
|
+
// docstring. `anchorId` is filled in once every member is known.
|
|
3308
|
+
anchorIndex: i,
|
|
3309
|
+
anchorId: ""
|
|
3310
|
+
};
|
|
3311
|
+
order.push(runId);
|
|
3312
|
+
}
|
|
3313
|
+
if (ref) {
|
|
3314
|
+
if (ref.name) g.name = ref.name;
|
|
3315
|
+
if (ref.path) g.path = ref.path;
|
|
3316
|
+
if (ref.mime) g.mime = ref.mime;
|
|
3317
|
+
if (typeof ref.size === "number") g.size = ref.size;
|
|
3318
|
+
if (ref.isReindex) g.isReindex = true;
|
|
3319
|
+
if (!ref.continued) g.mayHaveOlder = false;
|
|
3320
|
+
g.passCount++;
|
|
3321
|
+
}
|
|
3322
|
+
g.members.push({ msg, index: i });
|
|
3323
|
+
runOfIndex[i] = runId;
|
|
3324
|
+
if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
|
|
3325
|
+
if (ref && ref.name) keyByName[ref.name] = g.key;
|
|
3326
|
+
}
|
|
3327
|
+
for (var rk in runsOfKey) {
|
|
3328
|
+
var runIds = runsOfKey[rk];
|
|
3329
|
+
for (var ri = 0; ri < runIds.length; ri++) {
|
|
3330
|
+
var grpR = groups[runIds[ri]];
|
|
3331
|
+
if (!grpR) continue;
|
|
3332
|
+
var first = grpR.members[0];
|
|
3333
|
+
var firstId = first && first.msg && (first.msg._serverItemId || first.msg._localId);
|
|
3334
|
+
grpR.runKey = rk + "#" + (firstId || "n" + ri);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
for (var oi = 0; oi < order.length; oi++) {
|
|
3338
|
+
var grp = groups[order[oi]];
|
|
3339
|
+
var lastSettled = -1;
|
|
3340
|
+
for (var si = 0; si < grp.members.length; si++) {
|
|
3341
|
+
if (!isPendingMsg(grp.members[si].msg)) lastSettled = si;
|
|
3342
|
+
}
|
|
3343
|
+
var active = false;
|
|
3344
|
+
for (var mi = lastSettled + 1; mi < grp.members.length; mi++) {
|
|
3345
|
+
if (isPendingMsg(grp.members[mi].msg)) {
|
|
3346
|
+
active = true;
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
for (var xi = 0; xi < grp.members.length; xi++) {
|
|
3351
|
+
if (grp.members[xi].msg._cancelling) {
|
|
3352
|
+
grp.cancelling = true;
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
var seenIds = {};
|
|
3357
|
+
for (var ci = 0; ci < grp.members.length; ci++) {
|
|
3358
|
+
var cm = grp.members[ci].msg;
|
|
3359
|
+
if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
|
|
3360
|
+
if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
|
|
3361
|
+
if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
|
|
3362
|
+
if (ci < lastSettled) continue;
|
|
3363
|
+
if (seenIds[cm._serverItemId]) continue;
|
|
3364
|
+
seenIds[cm._serverItemId] = true;
|
|
3365
|
+
grp.cancellableIds.push(cm._serverItemId);
|
|
3366
|
+
}
|
|
3367
|
+
if (active) {
|
|
3368
|
+
grp.status = "active";
|
|
3369
|
+
} else {
|
|
3370
|
+
var last = grp.members[grp.members.length - 1].msg;
|
|
3371
|
+
grp.status = last.isError ? "error" : last.isCancelled ? "cancelled" : "done";
|
|
3372
|
+
}
|
|
3373
|
+
var sawFirstPass = false;
|
|
3374
|
+
for (var pi = 0; pi < grp.members.length; pi++) {
|
|
3375
|
+
var pm = grp.members[pi].msg;
|
|
3376
|
+
if (pm.role !== "user") continue;
|
|
3377
|
+
var pref = readFileRef(pm);
|
|
3378
|
+
if (pref && !pref.continued) {
|
|
3379
|
+
sawFirstPass = true;
|
|
3380
|
+
break;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
grp.mayHaveOlder = !sawFirstPass && hasMoreHistory;
|
|
3384
|
+
var anchor = grp.members[0];
|
|
3385
|
+
grp.anchorIndex = anchor.index;
|
|
3386
|
+
grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
|
|
3387
|
+
}
|
|
3388
|
+
var out = [];
|
|
3389
|
+
for (var j = 0; j < list.length; j++) {
|
|
3390
|
+
var r = runOfIndex[j];
|
|
3391
|
+
if (r === void 0) {
|
|
3392
|
+
out.push({ kind: "message", msg: list[j], index: j });
|
|
3393
|
+
continue;
|
|
3394
|
+
}
|
|
3395
|
+
if (groups[r].anchorIndex === j) out.push({ kind: "indexing", group: groups[r], index: j });
|
|
3396
|
+
}
|
|
3397
|
+
return out;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
2674
3400
|
// src/index.js
|
|
2675
3401
|
(function() {
|
|
2676
3402
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
2677
3403
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
2678
|
-
var BQ_VERSION = "1.
|
|
3404
|
+
var BQ_VERSION = "1.8.0" ;
|
|
2679
3405
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
2680
3406
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
2681
3407
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -3887,6 +4613,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3887
4613
|
if (CS.composerEl && CS.chatEl && CS.composerEl.parentNode !== CS.chatEl) CS.chatEl.appendChild(CS.composerEl);
|
|
3888
4614
|
renderMessages();
|
|
3889
4615
|
scrollToBottom();
|
|
4616
|
+
ensureHistoryFillsViewport();
|
|
3890
4617
|
}
|
|
3891
4618
|
function renderAccount() {
|
|
3892
4619
|
if (!CS.messagesBox) return;
|
|
@@ -4254,8 +4981,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4254
4981
|
}
|
|
4255
4982
|
var CS = {
|
|
4256
4983
|
messages: [],
|
|
4984
|
+
// Rendered .bq-message nodes, indexed BY MESSAGE INDEX (sparse: a message
|
|
4985
|
+
// folded into a collapsed indexing row has no node of its own).
|
|
4257
4986
|
messageEls: [],
|
|
4258
|
-
//
|
|
4987
|
+
// Expanded background-indexing rows, keyed by RUN (group.runKey), not by
|
|
4988
|
+
// file: re-indexing a file is a separate row and expands separately.
|
|
4989
|
+
indexGroupsOpen: {},
|
|
4259
4990
|
messagesBox: null,
|
|
4260
4991
|
// .bq-messages element
|
|
4261
4992
|
sending: false,
|
|
@@ -4325,14 +5056,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4325
5056
|
scrollToBottomIfSticky: function(smooth) {
|
|
4326
5057
|
return scrollToBottomIfSticky(smooth);
|
|
4327
5058
|
},
|
|
5059
|
+
// A first page can render shorter than the box (a file's every indexing
|
|
5060
|
+
// pass folds into ONE row), and a box that cannot scroll never fires the
|
|
5061
|
+
// scroll-to-top that is the sole trigger for loading page 2. Page out of
|
|
5062
|
+
// it here — only the view can measure.
|
|
5063
|
+
onHistoryLoaded: function(fetchMore, token) {
|
|
5064
|
+
if (!fetchMore) ensureHistoryFillsViewport(token);
|
|
5065
|
+
},
|
|
4328
5066
|
cancelRequest: function(opts) {
|
|
4329
5067
|
return S.skapi.cancelClientSecretRequest(opts);
|
|
4330
5068
|
},
|
|
4331
5069
|
refreshSession: function() {
|
|
4332
5070
|
return refreshSkapiSession();
|
|
4333
5071
|
},
|
|
4334
|
-
formatIndexingLabel: function(name, mime, size, storagePath, reindex) {
|
|
4335
|
-
return buildIndexingLabel(name, mime, size, storagePath, reindex);
|
|
5072
|
+
formatIndexingLabel: function(name, mime, size, storagePath, reindex, continued) {
|
|
5073
|
+
return buildIndexingLabel(name, mime, size, storagePath, reindex, continued);
|
|
4336
5074
|
},
|
|
4337
5075
|
isViewMounted: function() {
|
|
4338
5076
|
return !!CS.messagesBox;
|
|
@@ -4532,7 +5270,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4532
5270
|
if (!CS.messagesBox || CS.chatSettingsOpen) return;
|
|
4533
5271
|
var el = CS.messagesBox;
|
|
4534
5272
|
CS.stickToBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 16;
|
|
4535
|
-
if (el.scrollTop <= 60)
|
|
5273
|
+
if (el.scrollTop <= 60) pageOlderHistoryUntilTaller();
|
|
4536
5274
|
}
|
|
4537
5275
|
var _touchStartY = 0;
|
|
4538
5276
|
function onMessagesWheel(e) {
|
|
@@ -4545,23 +5283,6 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4545
5283
|
var y = e.touches && e.touches[0] ? e.touches[0].clientY : 0;
|
|
4546
5284
|
if (y > _touchStartY + 4) CS.stickToBottom = false;
|
|
4547
5285
|
}
|
|
4548
|
-
function normalizeTrailingInlineToken(value) {
|
|
4549
|
-
if (!value) return value;
|
|
4550
|
-
var out = value.replace(/[.,;:!?]+$/, "");
|
|
4551
|
-
var trimUnmatched = function(openCh, closeCh) {
|
|
4552
|
-
while (out.charAt(out.length - 1) === closeCh) {
|
|
4553
|
-
var openCount = (out.match(new RegExp("\\" + openCh, "g")) || []).length;
|
|
4554
|
-
var closeCount = (out.match(new RegExp("\\" + closeCh, "g")) || []).length;
|
|
4555
|
-
if (closeCount > openCount) out = out.slice(0, -1);
|
|
4556
|
-
else break;
|
|
4557
|
-
}
|
|
4558
|
-
};
|
|
4559
|
-
trimUnmatched("(", ")");
|
|
4560
|
-
trimUnmatched("[", "]");
|
|
4561
|
-
trimUnmatched("{", "}");
|
|
4562
|
-
out = out.replace(/[`'"*>]+$/, "");
|
|
4563
|
-
return out;
|
|
4564
|
-
}
|
|
4565
5286
|
function getOrCreateFileHref(filename, body) {
|
|
4566
5287
|
var key = filename + "\0" + body;
|
|
4567
5288
|
var existing = fileBlobCache.get(key);
|
|
@@ -4602,41 +5323,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4602
5323
|
return "<a " + attrs.join(" ") + ">" + escapeHtml(labelText) + "</a>";
|
|
4603
5324
|
}
|
|
4604
5325
|
function buildLinkPartFromGroups(full, g1, g2, g3, g4, g5, g6) {
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
var isUrl = /^https?:\/\//i.test(rawPath);
|
|
4611
|
-
if (isUrl && /^https:\/\//i.test(rawPath) && rawPath.toLowerCase().indexOf(dbHostPrefix.toLowerCase()) !== 0) {
|
|
4612
|
-
return { part: { type: "link", label: truncateLabelForDisplay(rawPath), fullLabel: rawPath, href: rawPath, expired: false }, tail };
|
|
5326
|
+
return classifyInlineLink(full, [g1, g2, g3, g4, g5, g6], {
|
|
5327
|
+
serviceId: S.serviceId,
|
|
5328
|
+
dbHostPrefix: "https://db." + hostDomain(),
|
|
5329
|
+
resolveFreshHref: function(expiredHref) {
|
|
5330
|
+
return refreshedExpiredLinkMap[expiredHref];
|
|
4613
5331
|
}
|
|
4614
|
-
|
|
4615
|
-
if (!remotePath) return null;
|
|
4616
|
-
var expiredHref = buildDisplayExpiredAttachmentHref(remotePath, remotePath);
|
|
4617
|
-
var cached = refreshedExpiredLinkMap[expiredHref];
|
|
4618
|
-
return { part: { type: "link", label: truncateLabelForDisplay(remotePath), fullLabel: remotePath, href: cached || expiredHref, expired: !cached, expiredHref, remotePath }, tail };
|
|
4619
|
-
}
|
|
4620
|
-
if (g4 && g5) {
|
|
4621
|
-
var rp = normalizeAttachmentPathCandidate(g5);
|
|
4622
|
-
if (!rp) return null;
|
|
4623
|
-
var eh = buildDisplayExpiredAttachmentHref(rp, rp);
|
|
4624
|
-
var c2 = refreshedExpiredLinkMap[eh];
|
|
4625
|
-
return { part: { type: "link", label: truncateLabelForDisplay(g4), fullLabel: g4, href: c2 || eh, expired: !c2, expiredHref: eh, remotePath: rp } };
|
|
4626
|
-
}
|
|
4627
|
-
var originalHref = g3 || g6 || "";
|
|
4628
|
-
if (!originalHref) return null;
|
|
4629
|
-
if (/^https:\/\//i.test(originalHref) && originalHref.toLowerCase().indexOf(dbHostPrefix.toLowerCase()) !== 0) {
|
|
4630
|
-
var plainLabel = g2 || originalHref;
|
|
4631
|
-
return { part: { type: "link", label: truncateLabelForDisplay(plainLabel), fullLabel: plainLabel, href: originalHref, expired: false } };
|
|
4632
|
-
}
|
|
4633
|
-
var rmp = extractRemotePathFromAttachmentHref(originalHref, S.serviceId);
|
|
4634
|
-
var fbLabel = g2 || originalHref;
|
|
4635
|
-
var ehref = rmp ? buildDisplayExpiredAttachmentHref(rmp, fbLabel) : originalHref;
|
|
4636
|
-
var cfresh = refreshedExpiredLinkMap[ehref];
|
|
4637
|
-
var expired = !!rmp && !cfresh;
|
|
4638
|
-
var fullLabel = rmp ? getExpiredAttachmentVisiblePath(rmp, g2 || originalHref) : g2 || originalHref;
|
|
4639
|
-
return { part: { type: "link", label: truncateLabelForDisplay(fullLabel), fullLabel, href: cfresh || ehref, expired, expiredHref: ehref, remotePath: rmp || void 0 } };
|
|
5332
|
+
});
|
|
4640
5333
|
}
|
|
4641
5334
|
function parseMsgPartsHtml(content) {
|
|
4642
5335
|
var placeholderHtml = [];
|
|
@@ -4702,9 +5395,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4702
5395
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
4703
5396
|
return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
4704
5397
|
}
|
|
4705
|
-
function buildIndexingLabel(name, mime, size, storagePath, reindex) {
|
|
4706
|
-
var extras = [];
|
|
5398
|
+
function buildIndexingLabel(name, mime, size, storagePath, reindex, continued) {
|
|
4707
5399
|
var nameLabel = storagePath ? "[" + name + "](" + storagePath + ")" : name;
|
|
5400
|
+
if (continued) return "Indexing (continuing) " + nameLabel;
|
|
5401
|
+
var extras = [];
|
|
4708
5402
|
if (mime) extras.push(mime);
|
|
4709
5403
|
if (size != null && size !== "" && !isNaN(Number(size))) extras.push(formatBytes(size));
|
|
4710
5404
|
return (reindex ? "Reindexing: " : "Indexing: ") + nameLabel + (extras.length ? " \xB7 " + extras.join(" \xB7 ") : "");
|
|
@@ -4775,16 +5469,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4775
5469
|
return S.skapi.deleteRecords({ service: S.serviceId, unique_id: "src::" + storagePath }).catch(function() {
|
|
4776
5470
|
});
|
|
4777
5471
|
}
|
|
4778
|
-
function getTemporaryUrlDb(path, expires) {
|
|
4779
|
-
|
|
5472
|
+
function getTemporaryUrlDb(path, expires, cdn) {
|
|
5473
|
+
var body = {
|
|
4780
5474
|
service: S.serviceId,
|
|
4781
5475
|
owner: S.owner,
|
|
4782
5476
|
request: "get-db",
|
|
4783
5477
|
key: path,
|
|
4784
|
-
expires: expires,
|
|
4785
|
-
contentType: mimeGetType(path) || "application/octet-stream"
|
|
4786
|
-
|
|
4787
|
-
|
|
5478
|
+
expires: expires || ATTACHMENT_URL_EXPIRES_SECONDS,
|
|
5479
|
+
contentType: mimeGetType(path) || "application/octet-stream"
|
|
5480
|
+
};
|
|
5481
|
+
if (cdn !== false) body.generate_temporary_cdn_url = true;
|
|
5482
|
+
return S.skapi.util.request("get-signed-url", body, { auth: true, method: "post" }).then(function(res) {
|
|
4788
5483
|
var u = typeof res === "string" ? res : res && res.url;
|
|
4789
5484
|
if (!u) throw new Error("No temporary URL returned.");
|
|
4790
5485
|
return "https://db." + hostDomain() + "/" + u;
|
|
@@ -5288,7 +5983,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5288
5983
|
}
|
|
5289
5984
|
function getPublicTemporaryUrl(remotePath) {
|
|
5290
5985
|
if (!remotePath) return Promise.reject(new Error("Missing attachment path."));
|
|
5291
|
-
return getTemporaryUrlDb(remotePath,
|
|
5986
|
+
return getTemporaryUrlDb(remotePath, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, false);
|
|
5987
|
+
}
|
|
5988
|
+
var refreshedLinkExpiryTimer = null;
|
|
5989
|
+
function expireAllRefreshedLinks() {
|
|
5990
|
+
for (var k in refreshedExpiredLinkMap) delete refreshedExpiredLinkMap[k];
|
|
5991
|
+
}
|
|
5992
|
+
function scheduleNextLinkExpiryBoundary() {
|
|
5993
|
+
if (refreshedLinkExpiryTimer) clearTimeout(refreshedLinkExpiryTimer);
|
|
5994
|
+
var now = Date.now();
|
|
5995
|
+
var next = Math.ceil(now / LINK_REFRESH_WINDOW_MS) * LINK_REFRESH_WINDOW_MS;
|
|
5996
|
+
refreshedLinkExpiryTimer = setTimeout(function() {
|
|
5997
|
+
expireAllRefreshedLinks();
|
|
5998
|
+
renderMessages();
|
|
5999
|
+
scheduleNextLinkExpiryBoundary();
|
|
6000
|
+
}, Math.max(1, next - now));
|
|
5292
6001
|
}
|
|
5293
6002
|
function fetchFreshHrefForExpiredLink(expiredHref, remotePath) {
|
|
5294
6003
|
var cached = refreshedExpiredLinkMap[expiredHref];
|
|
@@ -5301,6 +6010,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5301
6010
|
if (!resolved) return Promise.reject(new Error("Unable to refresh this expired attachment link."));
|
|
5302
6011
|
return getPublicTemporaryUrl(resolved).then(function(fresh) {
|
|
5303
6012
|
refreshedExpiredLinkMap[expiredHref] = fresh;
|
|
6013
|
+
scheduleNextLinkExpiryBoundary();
|
|
5304
6014
|
return fresh;
|
|
5305
6015
|
});
|
|
5306
6016
|
})().then(
|
|
@@ -5359,15 +6069,77 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5359
6069
|
if (key) lsSet(key, String(ts));
|
|
5360
6070
|
}
|
|
5361
6071
|
function fetchOlderHistoryIfNeeded() {
|
|
5362
|
-
if (CS.
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
6072
|
+
if (CS.historyEndOfList) return Promise.resolve(true);
|
|
6073
|
+
if (CS.loadingHistory || CS.loadingOlderHistory) return Promise.resolve(false);
|
|
6074
|
+
return session.loadHistory(true).then(function() {
|
|
6075
|
+
return true;
|
|
6076
|
+
});
|
|
6077
|
+
}
|
|
6078
|
+
function messagesBoxCanScroll() {
|
|
6079
|
+
if (!CS.messagesBox || CS.chatSettingsOpen) return true;
|
|
6080
|
+
return CS.messagesBox.scrollHeight - CS.messagesBox.clientHeight > HISTORY_FILL_SLACK_PX;
|
|
6081
|
+
}
|
|
6082
|
+
function topVisibleRowKey() {
|
|
6083
|
+
var box = CS.messagesBox;
|
|
6084
|
+
if (!box) return null;
|
|
6085
|
+
var boxTop = box.getBoundingClientRect().top;
|
|
6086
|
+
var kids = box.children;
|
|
6087
|
+
for (var i = 0; i < kids.length; i++) {
|
|
6088
|
+
var key = kids[i].getAttribute && kids[i].getAttribute("data-row-key");
|
|
6089
|
+
if (!key) continue;
|
|
6090
|
+
if (kids[i].getBoundingClientRect().top - boxTop + kids[i].offsetHeight > 0) return key;
|
|
6091
|
+
}
|
|
6092
|
+
return null;
|
|
6093
|
+
}
|
|
6094
|
+
function contentAboveRow(key) {
|
|
6095
|
+
var box = CS.messagesBox;
|
|
6096
|
+
if (!box) return null;
|
|
6097
|
+
var boxTop = box.getBoundingClientRect().top;
|
|
6098
|
+
var kids = box.children;
|
|
6099
|
+
for (var i = 0; i < kids.length; i++) {
|
|
6100
|
+
if (!kids[i].getAttribute || kids[i].getAttribute("data-row-key") !== key) continue;
|
|
6101
|
+
return kids[i].getBoundingClientRect().top - boxTop + box.scrollTop;
|
|
6102
|
+
}
|
|
6103
|
+
return null;
|
|
6104
|
+
}
|
|
6105
|
+
var _historyFillToken = 0;
|
|
6106
|
+
var _historyFiller = createHistoryFiller({
|
|
6107
|
+
isEndOfList: function() {
|
|
6108
|
+
return !!CS.historyEndOfList;
|
|
6109
|
+
},
|
|
6110
|
+
isLoading: function() {
|
|
6111
|
+
return !!(CS.loadingHistory || CS.loadingOlderHistory);
|
|
6112
|
+
},
|
|
6113
|
+
// The settings panel occupies the messages box and suppresses
|
|
6114
|
+
// renderMessages, so a fill started before it opened must stop.
|
|
6115
|
+
isStale: function() {
|
|
6116
|
+
return _historyFillToken !== CS.gateRefreshToken || !CS.messagesBox || CS.chatSettingsOpen;
|
|
6117
|
+
},
|
|
6118
|
+
messageCount: function() {
|
|
6119
|
+
return CS.messages.length;
|
|
6120
|
+
},
|
|
6121
|
+
fetchOlder: function() {
|
|
6122
|
+
return fetchOlderHistoryIfNeeded();
|
|
6123
|
+
}
|
|
6124
|
+
});
|
|
6125
|
+
function pageOlderHistoryUntil(isSatisfied, token) {
|
|
6126
|
+
if (token === void 0) token = CS.gateRefreshToken;
|
|
6127
|
+
if (token !== CS.gateRefreshToken) return Promise.resolve();
|
|
6128
|
+
_historyFillToken = token;
|
|
6129
|
+
return _historyFiller.fill(function() {
|
|
6130
|
+
return raf2().then(isSatisfied);
|
|
6131
|
+
});
|
|
6132
|
+
}
|
|
6133
|
+
function ensureHistoryFillsViewport(token) {
|
|
6134
|
+
return pageOlderHistoryUntil(messagesBoxCanScroll, token);
|
|
6135
|
+
}
|
|
6136
|
+
function pageOlderHistoryUntilTaller() {
|
|
6137
|
+
var anchorKey = topVisibleRowKey();
|
|
6138
|
+
var before = anchorKey ? contentAboveRow(anchorKey) : null;
|
|
6139
|
+
return pageOlderHistoryUntil(function() {
|
|
6140
|
+
if (!CS.messagesBox || !anchorKey || before === null) return true;
|
|
6141
|
+
var now = contentAboveRow(anchorKey);
|
|
6142
|
+
return now === null || now > before + HISTORY_FILL_SLACK_PX;
|
|
5371
6143
|
});
|
|
5372
6144
|
}
|
|
5373
6145
|
function openClearHistoryModal() {
|
|
@@ -5452,15 +6224,113 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5452
6224
|
});
|
|
5453
6225
|
bubble.appendChild(cancelBtn);
|
|
5454
6226
|
}
|
|
5455
|
-
var md = h("div", { class: "bq-md", html: parseMsgPartsHtml(msg.content) });
|
|
6227
|
+
var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
|
|
5456
6228
|
md.addEventListener("click", onBubbleLinkClick);
|
|
5457
6229
|
bubble.appendChild(md);
|
|
5458
6230
|
if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
5459
6231
|
if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
|
|
5460
6232
|
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
6233
|
+
var ts = formatChatTimestamp(msg._ts);
|
|
6234
|
+
if (ts) bubble.appendChild(h("time", { class: "bq-msg-time", text: ts }));
|
|
5461
6235
|
}
|
|
5462
6236
|
return h("div", { class: cls.join(" "), dataset: { msgIndex: String(idx) } }, bubble);
|
|
5463
6237
|
}
|
|
6238
|
+
var INDEX_ICON_ACTIVE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 12a8.5 8.5 0 0 1-8.5 8.5 8.5 8.5 0 0 1-7.6-4.7"/><path d="M3.5 12A8.5 8.5 0 0 1 12 3.5a8.5 8.5 0 0 1 7.6 4.7"/><path d="M20 3.6v4.8h-4.8"/><path d="M4 20.4v-4.8h4.8"/></svg>';
|
|
6239
|
+
var INDEX_ICON_DONE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12.5l5 5L20 6.5"/></svg>';
|
|
6240
|
+
var INDEX_ICON_ERROR = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5"/><path d="M12 16.6h.01"/></svg>';
|
|
6241
|
+
var INDEX_ICON_CANCELLED = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8.2 8.2l7.6 7.6"/></svg>';
|
|
6242
|
+
function indexGroupIcon(group) {
|
|
6243
|
+
if (group.status === "active") return INDEX_ICON_ACTIVE;
|
|
6244
|
+
if (group.status === "error") return INDEX_ICON_ERROR;
|
|
6245
|
+
if (group.status === "cancelled") return INDEX_ICON_CANCELLED;
|
|
6246
|
+
return INDEX_ICON_DONE;
|
|
6247
|
+
}
|
|
6248
|
+
function indexGroupVerb(group) {
|
|
6249
|
+
if (group.status === "active") return group.isReindex ? "Reindexing" : "Indexing";
|
|
6250
|
+
if (group.status === "error") return "Indexing failed:";
|
|
6251
|
+
if (group.status === "cancelled") return "Indexing cancelled:";
|
|
6252
|
+
return group.isReindex ? "Reindexed" : "Indexed";
|
|
6253
|
+
}
|
|
6254
|
+
function indexGroupLabel(group) {
|
|
6255
|
+
var nameLabel = group.path ? "[" + group.name + "](" + group.path + ")" : group.name;
|
|
6256
|
+
return indexGroupVerb(group) + " " + nameLabel;
|
|
6257
|
+
}
|
|
6258
|
+
function indexGroupCount(group) {
|
|
6259
|
+
if (group.passCount <= 1 && !group.mayHaveOlder) return "";
|
|
6260
|
+
return group.passCount + (group.mayHaveOlder ? "+" : "") + " passes";
|
|
6261
|
+
}
|
|
6262
|
+
function toggleIndexGroup(key) {
|
|
6263
|
+
if (CS.indexGroupsOpen[key]) delete CS.indexGroupsOpen[key];
|
|
6264
|
+
else CS.indexGroupsOpen[key] = true;
|
|
6265
|
+
renderMessages();
|
|
6266
|
+
ensureHistoryFillsViewport();
|
|
6267
|
+
}
|
|
6268
|
+
function buildIndexGroupEl(group, isOpen) {
|
|
6269
|
+
var cls = ["bq-index-group"];
|
|
6270
|
+
if (group.status === "active") cls.push("is-active");
|
|
6271
|
+
if (group.status === "error") cls.push("is-error");
|
|
6272
|
+
if (isOpen) cls.push("is-open");
|
|
6273
|
+
var label = h("span", { class: "bq-index-label", html: parseMsgPartsHtml(indexGroupLabel(group)) });
|
|
6274
|
+
label.addEventListener("click", function(e) {
|
|
6275
|
+
if (e.target && e.target.closest && e.target.closest("a")) e.stopPropagation();
|
|
6276
|
+
onBubbleLinkClick(e);
|
|
6277
|
+
});
|
|
6278
|
+
var cancelBtn = null;
|
|
6279
|
+
if (group.cancellableIds.length || group.cancelling) {
|
|
6280
|
+
cancelBtn = h("button", {
|
|
6281
|
+
class: "bq-index-cancel" + (group.cancelling ? " is-disabled" : ""),
|
|
6282
|
+
type: "button",
|
|
6283
|
+
title: group.cancelling ? "Stopping..." : "Stop indexing this file",
|
|
6284
|
+
"aria-label": "Stop indexing " + group.name,
|
|
6285
|
+
text: group.cancelling ? "Stopping..." : "Stop"
|
|
6286
|
+
});
|
|
6287
|
+
if (group.cancelling) cancelBtn.disabled = true;
|
|
6288
|
+
else cancelBtn.addEventListener("click", function(e) {
|
|
6289
|
+
e.stopPropagation();
|
|
6290
|
+
session.cancelIndexingGroup(group);
|
|
6291
|
+
});
|
|
6292
|
+
cancelBtn.addEventListener("keydown", function(e) {
|
|
6293
|
+
e.stopPropagation();
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
var head = h(
|
|
6297
|
+
"div",
|
|
6298
|
+
{
|
|
6299
|
+
class: "bq-index-head",
|
|
6300
|
+
role: "button",
|
|
6301
|
+
tabindex: "0",
|
|
6302
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
6303
|
+
title: isOpen ? "Hide indexing steps" : "Show indexing steps",
|
|
6304
|
+
onclick: function() {
|
|
6305
|
+
toggleIndexGroup(group.runKey);
|
|
6306
|
+
},
|
|
6307
|
+
onkeydown: function(e) {
|
|
6308
|
+
if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
|
|
6309
|
+
e.preventDefault();
|
|
6310
|
+
toggleIndexGroup(group.runKey);
|
|
6311
|
+
}
|
|
6312
|
+
},
|
|
6313
|
+
h("span", { class: "bq-index-icon", html: indexGroupIcon(group) }),
|
|
6314
|
+
label,
|
|
6315
|
+
indexGroupCount(group) ? h("span", { class: "bq-index-count", text: indexGroupCount(group) }) : null,
|
|
6316
|
+
cancelBtn,
|
|
6317
|
+
h("span", { class: "bq-index-chevron", text: "\u25B6" })
|
|
6318
|
+
);
|
|
6319
|
+
var el = h("div", { class: cls.join(" ") }, head);
|
|
6320
|
+
if (group.cancelError) {
|
|
6321
|
+
el.appendChild(h("div", {
|
|
6322
|
+
class: "bq-index-note is-error",
|
|
6323
|
+
text: "Could not stop this file: " + group.cancelError
|
|
6324
|
+
}));
|
|
6325
|
+
}
|
|
6326
|
+
if (isOpen && group.mayHaveOlder) {
|
|
6327
|
+
el.appendChild(h("div", {
|
|
6328
|
+
class: "bq-index-note",
|
|
6329
|
+
text: "Earlier passes of this file are further back in the conversation. Scroll up to load them."
|
|
6330
|
+
}));
|
|
6331
|
+
}
|
|
6332
|
+
return el;
|
|
6333
|
+
}
|
|
5464
6334
|
function historyLoadingEl(initial) {
|
|
5465
6335
|
if (initial) {
|
|
5466
6336
|
return h(
|
|
@@ -5476,9 +6346,53 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5476
6346
|
h("span", { class: "bq-loader" })
|
|
5477
6347
|
);
|
|
5478
6348
|
}
|
|
6349
|
+
function rowAnchorKey(msg, index) {
|
|
6350
|
+
if (!msg) return null;
|
|
6351
|
+
var id = msg._serverItemId || msg._localId;
|
|
6352
|
+
return id ? "s" + id + ":" + msg.role : "i" + index;
|
|
6353
|
+
}
|
|
6354
|
+
function indexGroupAnchorId(group) {
|
|
6355
|
+
return group.anchorId || "";
|
|
6356
|
+
}
|
|
6357
|
+
function captureScrollAnchor() {
|
|
6358
|
+
var box = CS.messagesBox;
|
|
6359
|
+
if (!box || CS.stickToBottom) return null;
|
|
6360
|
+
var boxTop = box.getBoundingClientRect().top;
|
|
6361
|
+
var kids = box.children;
|
|
6362
|
+
var fallback = null;
|
|
6363
|
+
for (var i = 0; i < kids.length; i++) {
|
|
6364
|
+
if (!kids[i].getAttribute) continue;
|
|
6365
|
+
var key = kids[i].getAttribute("data-row-key");
|
|
6366
|
+
if (!key) continue;
|
|
6367
|
+
var top = kids[i].getBoundingClientRect().top - boxTop;
|
|
6368
|
+
if (top + kids[i].offsetHeight <= 0) continue;
|
|
6369
|
+
var cand = { key, top, scrollTop: box.scrollTop, pos: kids[i].getAttribute("data-row-pos") };
|
|
6370
|
+
if (cand.pos === null) return cand;
|
|
6371
|
+
if (!fallback) fallback = cand;
|
|
6372
|
+
}
|
|
6373
|
+
return fallback || { key: null, top: 0, scrollTop: box.scrollTop };
|
|
6374
|
+
}
|
|
6375
|
+
function restoreScrollAnchor(anchor) {
|
|
6376
|
+
var box = CS.messagesBox;
|
|
6377
|
+
if (!box) return;
|
|
6378
|
+
if (!anchor || CS.stickToBottom) {
|
|
6379
|
+
box.scrollTop = box.scrollHeight;
|
|
6380
|
+
return;
|
|
6381
|
+
}
|
|
6382
|
+
var boxTop = box.getBoundingClientRect().top;
|
|
6383
|
+
var kids = box.children;
|
|
6384
|
+
for (var i = 0; anchor.key && i < kids.length; i++) {
|
|
6385
|
+
if (!kids[i].getAttribute || kids[i].getAttribute("data-row-key") !== anchor.key) continue;
|
|
6386
|
+
if (anchor.pos && kids[i].getAttribute("data-row-pos") !== anchor.pos) break;
|
|
6387
|
+
box.scrollTop += kids[i].getBoundingClientRect().top - boxTop - anchor.top;
|
|
6388
|
+
return;
|
|
6389
|
+
}
|
|
6390
|
+
box.scrollTop = anchor.scrollTop;
|
|
6391
|
+
}
|
|
5479
6392
|
function renderMessages() {
|
|
5480
6393
|
if (!CS.messagesBox) return;
|
|
5481
6394
|
if (CS.chatSettingsOpen) return;
|
|
6395
|
+
var anchor = captureScrollAnchor();
|
|
5482
6396
|
clear(CS.messagesBox);
|
|
5483
6397
|
CS.messageEls = [];
|
|
5484
6398
|
if (CS.loadingOlderHistory) CS.messagesBox.appendChild(historyLoadingEl(false));
|
|
@@ -5499,23 +6413,44 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5499
6413
|
CS.messagesBox.appendChild(greet);
|
|
5500
6414
|
return;
|
|
5501
6415
|
}
|
|
5502
|
-
CS.messages
|
|
5503
|
-
|
|
5504
|
-
|
|
6416
|
+
var rows = buildChatDisplayList(CS.messages, { hasMoreHistory: !CS.historyEndOfList });
|
|
6417
|
+
rows.forEach(function(row) {
|
|
6418
|
+
if (row.kind === "indexing") {
|
|
6419
|
+
var isOpen = !!CS.indexGroupsOpen[row.group.runKey];
|
|
6420
|
+
var groupEl = buildIndexGroupEl(row.group, isOpen);
|
|
6421
|
+
groupEl.setAttribute("data-row-key", "g" + row.group.runKey);
|
|
6422
|
+
groupEl.setAttribute("data-row-pos", indexGroupAnchorId(row.group));
|
|
6423
|
+
CS.messagesBox.appendChild(groupEl);
|
|
6424
|
+
if (!isOpen) return;
|
|
6425
|
+
row.group.members.forEach(function(member) {
|
|
6426
|
+
var pass = buildMessageEl(member.msg, member.index);
|
|
6427
|
+
pass.classList.add("bq-index-pass");
|
|
6428
|
+
pass.setAttribute("data-row-key", rowAnchorKey(member.msg, member.index));
|
|
6429
|
+
CS.messageEls[member.index] = pass;
|
|
6430
|
+
CS.messagesBox.appendChild(pass);
|
|
6431
|
+
});
|
|
6432
|
+
return;
|
|
6433
|
+
}
|
|
6434
|
+
var el = buildMessageEl(row.msg, row.index);
|
|
6435
|
+
el.setAttribute("data-row-key", rowAnchorKey(row.msg, row.index));
|
|
6436
|
+
CS.messageEls[row.index] = el;
|
|
5505
6437
|
CS.messagesBox.appendChild(el);
|
|
5506
6438
|
});
|
|
6439
|
+
restoreScrollAnchor(anchor);
|
|
5507
6440
|
}
|
|
5508
6441
|
function refreshMessageBubble(idx) {
|
|
5509
6442
|
if (idx < 0 || idx >= CS.messages.length) return;
|
|
5510
6443
|
var oldEl = CS.messageEls[idx];
|
|
5511
6444
|
if (!oldEl || !oldEl.parentNode) return;
|
|
5512
6445
|
var newEl = buildMessageEl(CS.messages[idx], idx);
|
|
6446
|
+
if (oldEl.classList.contains("bq-index-pass")) newEl.classList.add("bq-index-pass");
|
|
5513
6447
|
oldEl.parentNode.replaceChild(newEl, oldEl);
|
|
5514
6448
|
CS.messageEls[idx] = newEl;
|
|
5515
6449
|
}
|
|
5516
6450
|
function renderChat() {
|
|
5517
6451
|
CS.messages = [];
|
|
5518
6452
|
CS.messageEls = [];
|
|
6453
|
+
CS.indexGroupsOpen = {};
|
|
5519
6454
|
CS.sending = false;
|
|
5520
6455
|
CS.typing = false;
|
|
5521
6456
|
CS.typingAbort = true;
|
|
@@ -5933,6 +6868,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5933
6868
|
S._resizeBound = true;
|
|
5934
6869
|
window.addEventListener("resize", function() {
|
|
5935
6870
|
scheduleAttachmentOverflowRecompute();
|
|
6871
|
+
scrollToBottomIfSticky(false);
|
|
6872
|
+
ensureHistoryFillsViewport();
|
|
5936
6873
|
});
|
|
5937
6874
|
}
|
|
5938
6875
|
if (!S._visBound && typeof document !== "undefined" && document.addEventListener) {
|