bunnyquery 1.8.3 → 1.8.5
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 +38 -39
- package/bunnyquery.css +108 -2
- package/bunnyquery.js +1531 -269
- package/dist/engine.cjs +1276 -209
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +812 -53
- package/dist/engine.d.ts +812 -53
- package/dist/engine.mjs +1254 -210
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +11 -11
- package/src/engine/history.ts +23 -6
- package/src/engine/host.ts +63 -5
- package/src/engine/image_preview.ts +0 -0
- package/src/engine/index.ts +12 -0
- package/src/engine/indexing_groups.ts +323 -6
- package/src/engine/link_markup.ts +124 -0
- package/src/engine/links.ts +159 -26
- package/src/engine/prompts/chat_system_prompt.ts +24 -14
- package/src/engine/prompts/indexing_system_prompt.ts +19 -11
- package/src/engine/prompts/indexing_user_message.ts +32 -22
- package/src/engine/requests.ts +281 -14
- package/src/engine/session.ts +1158 -143
- package/src/engine/viewport_fill.ts +51 -4
- package/styles/chat.css +108 -2
package/bunnyquery.js
CHANGED
|
@@ -276,29 +276,39 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
|
|
|
276
276
|
|
|
277
277
|
// src/engine/prompts/chat_system_prompt.ts
|
|
278
278
|
function buildChatSystemPrompt(params) {
|
|
279
|
-
const {
|
|
279
|
+
const { projectId, serviceName, serviceDescription } = params;
|
|
280
280
|
let systemPrompt = `
|
|
281
|
-
You are a dedicated assistant for the project ID: "${
|
|
281
|
+
You are a dedicated assistant for the project ID: "${projectId}".
|
|
282
282
|
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.
|
|
283
283
|
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.
|
|
284
|
-
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records
|
|
284
|
+
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized" (where the indexer writes; pass access_group explicitly, including 0, to search another group), while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. 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 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. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
|
|
285
285
|
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.
|
|
286
|
-
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
|
|
286
|
+
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 filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - 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.
|
|
287
287
|
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.
|
|
288
288
|
- Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
|
|
289
289
|
- Most attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY had their text extracted on the server and inlined in the same message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read it directly there and do NOT call web_fetch for those files. A "[skapi: ...]" note in that block means the file could not be extracted.
|
|
290
290
|
- For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.
|
|
291
|
-
Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files,
|
|
292
|
-
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
|
|
293
|
-
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
|
|
294
|
-
|
|
291
|
+
Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files; it returns ONE window per call, so keep paging with the cursor from the previous window until it says END OF FILE before you conclude anything is absent. Be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as \xABPHOTO A88\xBB or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
|
|
292
|
+
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 - 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 - so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
|
|
293
|
+
File 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; every file extracted out of a document has one too, in table "__MEDIA__" (access_group "authorized"). Present each result as a markdown link as described above. Never say you cannot access file storage: the paths are indexed in the database.
|
|
294
|
+
Showing images: "show me the photo", "\uBCF4\uC5EC\uC918", "display it" is a request for the file's LINK, nothing more. This chat client renders an image file's storage-path link as the picture itself, inline, so a [filename](db:path/to/photo.jpg) link IS the image on screen. Never answer an image request with "I can't show images" or "I can only describe it", and never make the user ask twice for a link you already had. If you have the path, give the link and let the client paint it. The same is true of any file the user asks to see: the link is the answer. Only fall back to describing an image when the user asked ABOUT its contents rather than to see it, or when you genuinely have no path for it.
|
|
295
|
+
Media inside a document is extracted into real files: every embedded PICTURE inside an uploaded document - photos, diagrams, chart images - is pulled out at upload time and saved as its OWN permanent file in this project's storage, in the folder "__MEDIA__/<the document's storage path>/". Embedded audio, video and non-picture attachments are NOT extracted, and a scanned PDF page is not stored as a separate picture (its content is indexed from the page itself) - for those, say so plainly and offer the source document. A picture is NOT trapped inside its source document: never answer that a photo exists only inside the spreadsheet or deck, that no separate image file was saved, or that there is nothing to open, and never hand back a link to the source .xlsx or .pdf when the user asked for a picture inside it.
|
|
296
|
+
Finding an extracted media file: it is INDEXED, and its location is a stored VALUE. Get it by QUERYING, never by constructing a filename.
|
|
297
|
+
RECOGNISE IT BY THE VALUE, NOT THE FIELD NAME. Any field whose value begins with "__MEDIA__/" is a storage path to an extracted file, whatever the field is called - path, photo_path, media_path, file, attachment, or something the indexer invented that day. A record's unique_id beginning "src::__MEDIA__/" marks it as a media record too.
|
|
298
|
+
The reliable query is getRecords with reference "src::<the document's storage path>" - one call, every table, every access group. Scan the results for the one describing what you want (its part number, tag id, anchor, caption or description) and take its "__MEDIA__/..." value. Never let a table guess be the reason you report a file as missing.
|
|
299
|
+
Link it VERBATIM as [caption](db:<the path>). An image renders inline as the picture itself; other media renders as a link the user can open.
|
|
300
|
+
So "show me the photo of part X" is: find the record for that part, take its "__MEDIA__/..." value, link it.
|
|
301
|
+
IF THAT RECORD HAS NO PATH, JOIN ON LOCATION - this needs nothing to have been enriched. Every media record carries data.anchor (the cell or page it was embedded at), plus data.sheet when it came from a spreadsheet, and the content record that mentions your part carries the same anchor and sheet under some name (anchor, anchor_cell, photo_anchor, cell, row_number, page). So: read the anchor and sheet off the content record, query getRecords with reference "src::<the document>", and take the media record whose data.anchor, data.also_at or tags match the anchor, using data.sheet too when both records carry one. Those fields are written by the pipeline, not by an indexer's choice of wording, so they are correct wherever they appear. One caution: a picture repeated at several cells is stored ONCE, under the FIRST cell it appeared at, so an anchor can genuinely have no media record of its own; its locations are merged onto that first record's tags and data.also_at. Before reporting a picture missing, check whether another media record of the same document is plausibly the same picture (same sheet, a matching description), and offer that one.
|
|
302
|
+
THIS IS NOT ONLY ABOUT SPREADSHEET PHOTOS. Treat "show me the diagram in that deck" or "the picture in that PDF" exactly like a photo request: query for the media record, never reconstruct a filename. For embedded video, audio or a non-picture attachment there is no extracted file: say so plainly and offer the source document.
|
|
303
|
+
A document may still have no media record: it was indexed before the "__MEDIA__" table existed, or its format is one whose embedded files are not extracted. Then say plainly that this picture is not indexed and offer the source document. One missing record is never evidence that media is not stored.
|
|
304
|
+
File generation: When the user asks for DATABASE records as a file (CSV, spreadsheet, export, download), call exportRecordsToFile: it writes the rows on the server, keeps them out of your context, and returns a download_url you paste as the link. Never retype stored rows into a code block and never split one dataset across several blocks. For a file you are authoring yourself, or to produce specifically-formatted text such as HTML, CSV, JSON, or Markdown, 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 - never base64 or any other encoding. Example for CSV:
|
|
295
305
|
\`\`\`filename.csv
|
|
296
306
|
item,qty,total
|
|
297
307
|
Carrots,55,$38.50
|
|
298
308
|
Mushrooms,41,$73.80
|
|
299
309
|
Zucchini,29,$43.50
|
|
300
310
|
\`\`\`
|
|
301
|
-
The same pattern applies to any format
|
|
311
|
+
The same pattern applies to any format - name the block after the file you intend: \`\`\`my-data.json, \`\`\`index.html, \`\`\`sample.txt, and so on.`;
|
|
302
312
|
if (serviceDescription) {
|
|
303
313
|
systemPrompt += `
|
|
304
314
|
Project name: "${serviceName ?? ""}"
|
|
@@ -309,18 +319,26 @@ Project description: """${serviceDescription}"""`;
|
|
|
309
319
|
|
|
310
320
|
// src/engine/prompts/indexing_system_prompt.ts
|
|
311
321
|
function buildIndexingSystemPrompt(params) {
|
|
312
|
-
const {
|
|
313
|
-
let systemPrompt = `You are a background indexing agent for project ${
|
|
322
|
+
const { projectId, serviceName, serviceDescription } = params;
|
|
323
|
+
let systemPrompt = `You are a background indexing agent for project ${projectId}.
|
|
314
324
|
- 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.
|
|
315
325
|
- Most 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 been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
|
|
316
|
-
- BIG SPREADSHEETS / TEXT: the inline content may be only the FIRST part of a large file (it can end with a truncation or "more remains" note).
|
|
326
|
+
- BIG SPREADSHEETS / TEXT: the inline content may be only the FIRST part of a large file (it can end with a truncation or "more remains" note). UNLESS this message already embeds a window of the file (in which case the message tells you not to call readFileContent, and you must not), read big spreadsheets and big text/data files WITH THE readFileContent TOOL: it returns the file ONE WINDOW at a time (spreadsheets as coordinate-tagged grid rows, text as a range of characters). Pass the file's storage path. After each window: datafy it into records and SAVE them, THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed - never stop after the first window. (Do NOT call readFileContent on a PDF - see the next line.)
|
|
317
327
|
- PDFs (scanned or not): you do NOT read a PDF with a tool or a URL. Its pages are RENDERED and embedded directly in the user message as IMAGE blocks, a WINDOW of pages at a time. LOOK at the embedded page images and datafy every one. The note beside them tells you whether MORE pages remain: if so, save this window's records and stop (a follow-up pass shows the next window automatically); only when the note says it was the LAST window is the PDF fully seen. Do NOT call readFileContent or web_fetch for a PDF.
|
|
318
328
|
- VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
|
|
319
|
-
-
|
|
320
|
-
-
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
-
|
|
329
|
+
- TRANSCRIBE, DO NOT DESCRIBE. When an image contains ANY text - a label, tag, stamp, form field, serial/part number, handwriting - your FIRST job is to read the characters out and store them VERBATIM, not to describe the scene. A record saying "a red inspection tag with handwritten markings" is worthless: it is unsearchable and every such photo produces the same sentence. Put the characters you can actually read into these EXACT fields, not variations of them: "printed_text" (the pre-printed wording), "handwritten_text" (what a person wrote by hand), and, when you can resolve one, "part_no", "tag_id" and "date". Same reason as the fixed table names: a field called photo_text in one pass and visible_text_notes in the next cannot be queried together. Read PARTIAL values rather than skipping: "500.7402.52__" beats nothing. Only when a character is genuinely unreadable, leave that field null or mark the unreadable span - do NOT invent it, and do NOT replace the whole transcription with a description of what the object looks like. A scene description is a nice extra AFTER the text, never instead of it.
|
|
330
|
+
- IMAGE FILES uploaded as the file itself: if ANY readable character appears ANYWHERE in the image (a label, a stamp, a sign in the background) it counts as an image WITH text - transcribe it per the rule above, and also capture the layout (what appears where) and every entity named. Only a truly text-free image gets description first: a one-line caption, then the objects present with their attributes (type, color, count, condition, position). Either way, save what you extract onto the file's "src::" record with updateRecords, TAG every entity and identifier visible, and INDEX the one number the image offers (a measured value, an amount, a count).
|
|
331
|
+
- Whatever the file type, this file's identity is "src::" + its storage path (the "storage path" metadata line) - never the inline content or a temporary URL. That record ALREADY EXISTS: the upload pipeline creates it in table "file_summaries" (access group "authorized") before indexing starts, so posting it again is rejected as a duplicate unique_id. Reference it from every record you write, and add what you learn to it with updateRecords. If that update unexpectedly reports the record does not exist, post it yourself ONCE with that exact "src::" unique_id (table "file_summaries", access group "authorized") and carry on; this is the ONE exception to the do-NOT-post-the-file-record rules elsewhere in these instructions, because the source identity must never be dropped just because an update failed.
|
|
332
|
+
- REACHABILITY (hard rule): every record you write while indexing this file MUST be reachable from the file's "src::<storage path>" record by following reference - either reference that record directly, or reference something that already reaches it. A record with no reference, or one pointing outside this file's chain, is an ORPHAN: deleting or re-indexing the file removes the reachable records and leaves the orphan behind forever, where it keeps turning up in later answers as stale data. If you create an intermediate record that OTHER records reference (a page record that rows hang off, a sheet or section record), set source.can_remove_referencing_records to true on it; the delete cascade passes a delete through a record only when that record carries the flag OR a unique_id starting "src::" (the file record cascades because its unique_id starts with "src::"; the intermediates you create carry no "src::" id, so they need the flag), and it cascades ONE LEVEL AT A TIME, so EVERY intermediate record in a chain needs its own marker - an unmarked link stops the cascade there and everything below it survives as orphans. When in doubt, reference the file record directly and keep the chain flat.
|
|
333
|
+
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a table named EXACTLY "spreadsheet_rows". Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, window by window, until it ends. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. The file-level "src::" record ALREADY EXISTS - the upload pipeline creates it before indexing starts - so do NOT create it. Link EVERY per-row record to it via reference (set each row record's reference to exactly "src::" + the storage path, with NO sheet/window/summary suffix added; the row records themselves do NOT carry a src:: unique_id). Enrich that same record with sheet name(s), column headers and total row count via updateRecords rather than posting another one. The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed. INDEX each row record on the row's most useful NUMERIC column (named by its header) so rows sort and range-query; when the row has no numeric column, index the grid row number instead. TAG each row record with the sheet name, the file name, and the row's categorical values (a status, a category, a type) - tags are how rows are filtered without scanning the table.
|
|
334
|
+
- ONE RECORD PER GRID ROW, ALWAYS. "Row" means the numbered row of the sheet (R37 is one record), never a visual block, item, section or left/right pair. Sheets that repeat the same columns side by side (an A/B block beside a C/D block, "paired" or "mirrored" layouts) still get ONE record per grid row, holding BOTH sides - suffix the keys to keep them apart (PART_NO_A / PART_NO_B). Collapsing a 16-row window into 2 or 3 "block" records is the single most damaging mistake here: it silently loses most of the cells and makes every later total wrong, because some windows were counted per row and others per block. If a window shows rows R37 to R52, you save records for R37..R52 and the count you report is the number of grid rows you actually wrote.
|
|
335
|
+
- FIXED TABLE NAMES. Never invent a table name for one pass, and never vary the name between passes of the SAME file: that scatters one file's data across tables nobody can enumerate later, so the data is effectively lost even though every save succeeded. Use exactly "spreadsheet_rows" for spreadsheet row records, "book_chapters" for a chapter record, and "file_summaries" for the file-level record (which already exists, so update it and never post it). Embedded photos and other embedded files get NO table of your choosing: their records already exist in table "__MEDIA__", see EXTRACTED MEDIA below. For a content type none of those fit, choose ONE plain descriptive name, use that same name for every pass of the file, and never mint variants of it (inspection_items / item_records / sheet_items / inspection_data are four names for what is one table).
|
|
336
|
+
- EXTRACTED MEDIA: every PICTURE embedded in an uploaded document (photos, diagrams, chart images) is pulled out and saved as a real permanent file under "__MEDIA__/<the document's storage path>/<name>", and a record for each one ALREADY EXISTS in table "__MEDIA__" with unique_id "src::<that path>", reference "src::<the document>", and its path, anchor and sheet already in data. Do NOT create it - the unique_id is taken and your post is rejected. UPDATE it with updateRecords, addressed by that unique_id, adding what the file actually SHOWS plus TAGS for every identifier visible in it (part numbers, tag ids, item names, serial numbers). An update REPLACES the fields you send, so send the existing tags back with your new ones and keep every field already in data (path, anchor, sheet, source, mime, bytes). ONE FILE, ONE RECORD: never also create a photo record in another table. If the update reports that the record does not exist, create it with that same unique_id, reference and data.path - the path must never be lost. Audio and video clips and non-picture attachments are NOT extracted, so never claim a separate file or a "__MEDIA__" record exists for one of those.
|
|
337
|
+
- AUDIO files: transcribe the speech, and capture speakers (named where identifiable), the topics discussed, and timestamps of key moments in the record's data. TAG the language, the audio type (call, meeting, dictation, music), each speaker and every named entity; INDEX the duration in seconds as duration_seconds. VIDEO files: everything audio gets, PLUS transcribe on-screen text verbatim (same transcription discipline as photos) and capture the visual timeline - scene changes and what each scene shows, with timestamps. Same tags as audio plus every entity visible on screen, and INDEX duration_seconds here too. These audio and video rules apply to files UPLOADED AS FILES: the transcript and timeline land on the file's own "src::" record, which already exists. Audio or video embedded inside a document is NOT extracted, so never look for or promise a "__MEDIA__" record for it.
|
|
338
|
+
- EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in the table "book_chapters" - never collapse the whole book into a single record. INDEX each chapter record on its chapter number (so chapters sort and range-query in order) and include the chapter title among its tags; the record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO put the book-level facts (title, author, language, overall summary, chapter list / table of contents, genre/subjects) onto the "src::" file record that ALREADY EXISTS in "file_summaries", using updateRecords. Do NOT post a second book-level record, and set every chapter record's reference to exactly "src::" + the storage path. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
|
|
339
|
+
- URL SOURCES: when the source being indexed is a URL rather than an uploaded file (a temporary or signed URL that merely DELIVERS an uploaded file's bytes is not a URL source; that file keeps its storage-path identity), its identity is "src::" + the FULL URL INCLUDING the query string (the query string often selects the content, so dropping it collapses different pages into one identity). If no record with that unique_id exists, create it; if the slot is already taken, update that record or reference it - never mint a variant id. For a WEB PAGE: extract everything on it, infer the page's primary entity type when it is not obvious (product, listing, article, profile), TAG that entity type plus the entities on the page, and INDEX the ONE number every entity of that type can be compared by (a price for a product, a date for an article). Any OTHER URL (a file behind a link) is downloaded and indexed under whichever per-type rule above matches its content. When the URL's content offers more index points than one record carries, add reference-linked records reachable from its "src::" record.
|
|
340
|
+
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far; a follow-up pass will automatically continue from where you stopped. NEVER store raw or encoded file bytes in ANY field: no base64, no data: URIs, no hex or blob dumps. A long opaque non-human-readable string is not data - replace it with a structured description of what it encodes. If base64 or a data: URI is all you have for something, describe it conceptually and never paste it; if nothing human-readable can be extracted at all, OMIT that record rather than saving noise.
|
|
341
|
+
- COMPLETION SIGNAL: only when YOU paged the file yourself with readFileContent and it reported "END OF FILE", with every row/item saved, end your final message with the token INDEXING_COMPLETE on its own line. If more rows remain, do NOT write that token - leaving it out is how the system knows to run another pass to continue. When the file arrives INSIDE this message one window at a time (an embedded window of rows/text, or rendered PDF page images), you are NOT the one who decides it is finished: the system advances the window off the real page/row count and sends the next pass automatically, so save this window, report what you saved, and never imply you have seen the whole file.
|
|
324
342
|
- Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
|
|
325
343
|
if (serviceDescription) {
|
|
326
344
|
systemPrompt += `
|
|
@@ -342,7 +360,7 @@ File metadata:
|
|
|
342
360
|
` : "");
|
|
343
361
|
if (options?.inlineContent) {
|
|
344
362
|
return head + `
|
|
345
|
-
The file's content was parsed by the client and is provided inline below. Read it directly
|
|
363
|
+
The file's content was parsed by the client and is provided inline below. Read it directly - do NOT fetch any URL for this file. Set every record's reference to exactly "src::" + the storage path above (not this content). That file record already exists, so enrich it with updateRecords rather than posting it.
|
|
346
364
|
|
|
347
365
|
----- BEGIN FILE CONTENT -----
|
|
348
366
|
${options.inlineContent}
|
|
@@ -350,7 +368,7 @@ ${options.inlineContent}
|
|
|
350
368
|
}
|
|
351
369
|
if (options?.inlineContentPlaceholder) {
|
|
352
370
|
return head + `
|
|
353
|
-
The file's text content was extracted on the server and is provided inline below. Read it directly
|
|
371
|
+
The file's text content was extracted on the server and is provided inline below. Read it directly - do NOT fetch any URL for this file. Set every record's reference to exactly "src::" + the storage path above (not this content). That file record already exists, so enrich it with updateRecords rather than posting it.
|
|
354
372
|
|
|
355
373
|
----- BEGIN FILE CONTENT -----
|
|
356
374
|
${options.inlineContentPlaceholder}
|
|
@@ -358,7 +376,7 @@ ${options.inlineContentPlaceholder}
|
|
|
358
376
|
}
|
|
359
377
|
if (options?.pagedRead) {
|
|
360
378
|
return head + `
|
|
361
|
-
Read this file with the readFileContent tool, using the storage path above - do NOT fetch a URL and do NOT rely on a single sample. readFileContent returns the file ONE WINDOW at a time: spreadsheets as coordinate-tagged grid rows (e.g. 'R4 A:E&I NUMBER | B:E1007'), scanned/large PDFs as rendered PAGE IMAGES, and windows may include embedded photos - LOOK at any images and datafy what they show. Page through EVERY window: for each window SAVE records for its rows/items/pages (postRecords, one record per row/item), THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed. Do NOT stop after the first window and do NOT just write a summary.
|
|
379
|
+
Read this file with the readFileContent tool, using the storage path above - do NOT fetch a URL and do NOT rely on a single sample. readFileContent returns the file ONE WINDOW at a time: spreadsheets as coordinate-tagged grid rows (e.g. 'R4 A:E&I NUMBER | B:E1007'), scanned/large PDFs as rendered PAGE IMAGES, and windows may include embedded photos - LOOK at any images and datafy what they show. Page through EVERY window: for each window SAVE records for its rows/items/pages (postRecords, one record per row/item), THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed. Do NOT stop after the first window and do NOT just write a summary. Set every record's reference to exactly "src::" + the storage path above; that file record already exists, so enrich it with updateRecords instead of posting it again.` + (attachment.url ? `
|
|
362
380
|
(A temporary URL is provided ONLY as a fallback if readFileContent fails: ${attachment.url})` : "");
|
|
363
381
|
}
|
|
364
382
|
return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
|
|
@@ -393,7 +411,11 @@ Records for the earlier pages are ALREADY saved (they reference "${src}"). The N
|
|
|
393
411
|
return `
|
|
394
412
|
${placeholder}
|
|
395
413
|
|
|
396
|
-
LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams).
|
|
414
|
+
LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Set EVERY record's reference to exactly "src::" + the storage path above. That file record ALREADY EXISTS, so do NOT post it, and do NOT give your page records a "src::" unique_id of their own. A record with no reference back to it is an ORPHAN: re-indexing the file deletes the linked records and leaves the orphan behind forever as stale data.
|
|
415
|
+
|
|
416
|
+
Each image is preceded by a label giving its DOCUMENT PAGE number. That label is the page's identity - use it, and ignore any page number PRINTED on the document itself (a scan often restarts its own numbering per section, so a footer reading "PAGE 4 OF 8" routinely disagrees with the real position). Whether a page is one you have already saved is stated in the note above the images - decide from that, never from a printed page number.
|
|
417
|
+
|
|
418
|
+
Transcribe COMPLETELY, not representatively. A table with twenty rows gets twenty records, not a sample of the first few - if a page has more rows than you can save comfortably, still save them all rather than summarising. Where a page carries an embedded text layer it is quoted above that page's image: it is the exact text and should be preferred over reading the pixels, with the image used for layout, tables, stamps and handwriting.
|
|
397
419
|
|
|
398
420
|
Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read the rest of the file and do NOT worry about the pages after this window: if any remain, the next window is rendered and sent to you automatically. Report only the pages you were actually shown - never imply you have seen the whole document.`;
|
|
399
421
|
}
|
|
@@ -412,9 +434,9 @@ This file is delivered to you ONE WINDOW at a time, embedded directly in this me
|
|
|
412
434
|
return head + buildRenderMeta(attachment) + where + `
|
|
413
435
|
${placeholder}
|
|
414
436
|
|
|
415
|
-
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.
|
|
437
|
+
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. The file-level record ALREADY EXISTS with unique_id "src::" + the storage path above: do NOT post it (a duplicate unique_id is rejected), enrich it with updateRecords, and link every row/section record to it by reference.
|
|
416
438
|
|
|
417
|
-
If this window has PHOTOS attached as images, LOOK at each one and datafy what it actually shows
|
|
439
|
+
If this window has PHOTOS attached as images, LOOK at each one and datafy what it actually shows. A \xABPHOTO ...\xBB marker in the grid text ties a picture to its row and comes in two forms. \xABPHOTO A88 -> __MEDIA__/...\xBB means the picture at cell A88 is saved as a permanent file at exactly that storage path, and its record in table "__MEDIA__" has unique_id "src::" + that path: UPDATE that record with updateRecords, adding what the picture SHOWS and TAGS for every identifier visible in it (part numbers, tag ids, item names, serial numbers). Do NOT create a duplicate and do NOT add a second photo record in another table: one file, one record. If that update reports the record does not exist, create it ONCE with that same unique_id, reference "src::" + the storage path above, table "__MEDIA__", access group "authorized", and data carrying the path - the path must never be lost. A bare \xABPHOTO A88\xBB marker with no arrow is a picture with no stored path of its own in this window: usually a repeat stored under an earlier anchor, or one too small to keep. NEVER construct a storage path or unique_id for it: find its record, if any, with getRecords reference "src::" + the storage path above, matching the cell against data.anchor or tags, and enrich what you find. The row record stays about its row's cells. Never report that photo contents could not be extracted when images are attached here.
|
|
418
440
|
|
|
419
441
|
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.`;
|
|
420
442
|
}
|
|
@@ -427,11 +449,10 @@ File metadata:
|
|
|
427
449
|
- storage path: ${attachment.storagePath}
|
|
428
450
|
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
429
451
|
` : "") + `
|
|
430
|
-
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. Do NOT re-save windows that are already saved. Use the storage path above for the "src::" unique_id. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
|
|
452
|
+
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest row/window already saved). The reference ALONE is the whole query: it returns every record written from this file across ALL tables and ALL access groups, so do NOT add table_name or access_group to narrow it. The response is PAGED, so keep fetching pages until it reports there are no more, and take the furthest point from the WHOLE set, never from the first page. Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
|
|
453
|
+
- Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
|
|
454
|
+
- Text: the cursor is the character offset already read.
|
|
455
|
+
Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. A \xABPHOTO <cell>\xBB marker in a window marks an embedded picture whose extracted file already has a record in table "__MEDIA__": find it with getRecords reference "src::" + the storage path above and match the cell against data.anchor or tags (a repeated picture is stored under its first anchor only), then enrich it with updateRecords. Never create a photo record of your own and never construct a path for one. Do NOT re-save windows that are already saved. Set every record's reference to exactly "src::" + the storage path above (no sheet, window or summary suffix added). That file record already exists, so do NOT post it; enrich it with updateRecords. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
|
|
435
456
|
}
|
|
436
457
|
|
|
437
458
|
// src/engine/errors.ts
|
|
@@ -545,6 +566,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
545
566
|
var EXPIRED_ATTACHMENT_URL_ORIGIN = "https://" + EXPIRED_ATTACHMENT_URL_HOST;
|
|
546
567
|
var LINK_LABEL_MAX_DISPLAY_CHARS = 32;
|
|
547
568
|
var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS = 20 * 60;
|
|
569
|
+
var PREVIEW_BROWSER_CACHE_SECONDS = 7 * 24 * 60 * 60;
|
|
548
570
|
var LINK_REFRESH_WINDOW_MS = (EXPIRED_LINK_REFRESH_EXPIRES_SECONDS - 5 * 60) * 1e3;
|
|
549
571
|
function createInlineLinkRegex() {
|
|
550
572
|
return /src::(\S+)|\[([^\]\n]+)\]\((https?:\/\/(?:[^\s()]|\([^\s()]*\))+)\)|\[([^\]\n]+)\]\(((?:[^()\n]|\([^()\n]*\))+)\)|(https?:\/\/[^\s<>"']+)/g;
|
|
@@ -564,7 +586,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
564
586
|
function normalizeAttachmentPathCandidate(value) {
|
|
565
587
|
return safeDecodeURIComponent((value || "").trim()).replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
566
588
|
}
|
|
567
|
-
function extractRemotePathFromAttachmentHref(href,
|
|
589
|
+
function extractRemotePathFromAttachmentHref(href, projectId) {
|
|
568
590
|
try {
|
|
569
591
|
var parsed = new URL(href);
|
|
570
592
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
@@ -572,7 +594,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
572
594
|
var segs = path.split("/").filter(Boolean);
|
|
573
595
|
if (!segs.length) return null;
|
|
574
596
|
var HEX = /^[a-f0-9]{32,}$/i;
|
|
575
|
-
var sid =
|
|
597
|
+
var sid = projectId || "";
|
|
576
598
|
var start = 0;
|
|
577
599
|
while (start < segs.length) {
|
|
578
600
|
var seg = segs[start];
|
|
@@ -596,13 +618,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
596
618
|
function buildDisplayExpiredAttachmentHref(remotePath, fallback) {
|
|
597
619
|
return EXPIRED_ATTACHMENT_URL_ORIGIN + "/" + encodePathSegments(getExpiredAttachmentVisiblePath(remotePath, fallback));
|
|
598
620
|
}
|
|
599
|
-
function isServiceDbAttachmentHref(href,
|
|
600
|
-
if (!
|
|
621
|
+
function isServiceDbAttachmentHref(href, projectId) {
|
|
622
|
+
if (!projectId) return false;
|
|
601
623
|
try {
|
|
602
624
|
var parsed = new URL(href);
|
|
603
625
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
604
626
|
var segs = normalizeAttachmentPathCandidate(parsed.pathname || "").split("/").filter(Boolean);
|
|
605
|
-
return segs.length > 0 && segs[0] ===
|
|
627
|
+
return segs.length > 0 && segs[0] === projectId;
|
|
606
628
|
} catch (e) {
|
|
607
629
|
return false;
|
|
608
630
|
}
|
|
@@ -617,12 +639,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
617
639
|
return null;
|
|
618
640
|
}
|
|
619
641
|
}
|
|
620
|
-
function sanitizeAttachmentLinksForHistory(content,
|
|
642
|
+
function sanitizeAttachmentLinksForHistory(content, projectId, forAssistant) {
|
|
621
643
|
if (!content) return content;
|
|
622
644
|
if (!forAssistant && content.indexOf("Attached files:") === -1) return content;
|
|
623
645
|
return content.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, function(_m, label, href) {
|
|
624
|
-
if (!isServiceDbAttachmentHref(href,
|
|
625
|
-
var remotePath = extractRemotePathFromAttachmentHref(href,
|
|
646
|
+
if (!isServiceDbAttachmentHref(href, projectId)) return _m;
|
|
647
|
+
var remotePath = extractRemotePathFromAttachmentHref(href, projectId);
|
|
626
648
|
var fullPath = remotePath || normalizeAttachmentPathCandidate(label);
|
|
627
649
|
if (!fullPath) return _m;
|
|
628
650
|
return "[" + label + "](" + buildDisplayExpiredAttachmentHref(fullPath, label) + ")";
|
|
@@ -663,6 +685,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
663
685
|
out = out.replace(/[`'"*>]+$/, "");
|
|
664
686
|
return out;
|
|
665
687
|
}
|
|
688
|
+
var PREVIEWABLE_IMAGE_CONTENT_TYPES = {
|
|
689
|
+
png: "image/png",
|
|
690
|
+
jpg: "image/jpeg",
|
|
691
|
+
jpeg: "image/jpeg",
|
|
692
|
+
gif: "image/gif",
|
|
693
|
+
webp: "image/webp",
|
|
694
|
+
avif: "image/avif",
|
|
695
|
+
bmp: "image/bmp"
|
|
696
|
+
};
|
|
697
|
+
function previewableExtOf(nameOrPath) {
|
|
698
|
+
var v = String(nameOrPath || "");
|
|
699
|
+
var cut = v.search(/[?#]/);
|
|
700
|
+
if (cut !== -1) v = v.slice(0, cut);
|
|
701
|
+
v = v.replace(/[\\/]+$/, "");
|
|
702
|
+
var dot = v.lastIndexOf(".");
|
|
703
|
+
if (dot <= 0) return "";
|
|
704
|
+
var ext = v.slice(dot + 1).trim().toLowerCase();
|
|
705
|
+
return /^[a-z0-9]+$/.test(ext) ? ext : "";
|
|
706
|
+
}
|
|
666
707
|
function classifyInlineLink(full, groups, ctx) {
|
|
667
708
|
var g1 = groups[0], g2 = groups[1], g3 = groups[2], g4 = groups[3], g5 = groups[4], g6 = groups[5];
|
|
668
709
|
var dbHostPrefix = (ctx.dbHostPrefix || "").toLowerCase();
|
|
@@ -676,17 +717,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
676
717
|
if (!remotePath2) return null;
|
|
677
718
|
var expiredHref = buildDisplayExpiredAttachmentHref(remotePath2, label);
|
|
678
719
|
var cached = fresh(expiredHref);
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
remotePath: remotePath2
|
|
688
|
-
}
|
|
720
|
+
var part = {
|
|
721
|
+
type: "link",
|
|
722
|
+
label: truncateLabelForDisplay(label),
|
|
723
|
+
fullLabel: label,
|
|
724
|
+
href: cached || expiredHref,
|
|
725
|
+
expired: !cached,
|
|
726
|
+
expiredHref,
|
|
727
|
+
remotePath: remotePath2
|
|
689
728
|
};
|
|
729
|
+
var ext = previewableExtOf(remotePath2);
|
|
730
|
+
var ct = PREVIEWABLE_IMAGE_CONTENT_TYPES[ext];
|
|
731
|
+
if (ct) part.image = { ext, contentType: ct };
|
|
732
|
+
return { part };
|
|
690
733
|
};
|
|
691
734
|
if (g1) {
|
|
692
735
|
var rawPath = normalizeTrailingInlineToken(g1);
|
|
@@ -699,14 +742,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
699
742
|
tail
|
|
700
743
|
};
|
|
701
744
|
}
|
|
702
|
-
var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.
|
|
745
|
+
var srcPath = readExpiredAttachmentHref(rawPath) || (srcIsUrl ? extractRemotePathFromAttachmentHref(rawPath, ctx.projectId) || normalizeAttachmentPathCandidate(rawPath) : rawPath.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+/g, "/"));
|
|
703
746
|
var srcBuilt = asStoredFile(srcPath, srcPath);
|
|
704
747
|
return srcBuilt ? { part: srcBuilt.part, tail } : null;
|
|
705
748
|
}
|
|
706
749
|
if (g4 && g5) {
|
|
707
750
|
var dbTarget = /^db:(.+)$/i.exec(g5.trim());
|
|
708
751
|
if (dbTarget) {
|
|
709
|
-
var
|
|
752
|
+
var rawDbPath = dbTarget[1].trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
753
|
+
var declared = asStoredFile(rawDbPath, g4);
|
|
710
754
|
if (!declared) return null;
|
|
711
755
|
declared.part.label = truncateLabelForDisplay(g4);
|
|
712
756
|
declared.part.fullLabel = g4;
|
|
@@ -751,8 +795,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
751
795
|
return withTail(carriedBuilt);
|
|
752
796
|
}
|
|
753
797
|
}
|
|
754
|
-
if (isServiceDbAttachmentHref(originalHref, ctx.
|
|
755
|
-
var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.
|
|
798
|
+
if (isServiceDbAttachmentHref(originalHref, ctx.projectId)) {
|
|
799
|
+
var remotePath = extractRemotePathFromAttachmentHref(originalHref, ctx.projectId);
|
|
756
800
|
if (remotePath) {
|
|
757
801
|
var dbBuilt = asStoredFile(remotePath, getExpiredAttachmentVisiblePath(remotePath, urlLabel));
|
|
758
802
|
if (dbBuilt) return withTail(dbBuilt);
|
|
@@ -762,6 +806,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
762
806
|
part: { type: "link", label: truncateLabelForDisplay(urlLabel), fullLabel: urlLabel, href: originalHref, expired: false }
|
|
763
807
|
});
|
|
764
808
|
}
|
|
809
|
+
function linkUnavailableKeyForPath(remotePath) {
|
|
810
|
+
return "path:" + (remotePath || "");
|
|
811
|
+
}
|
|
812
|
+
function linkUnavailableKeyForHref(href) {
|
|
813
|
+
return "href:" + (href || "");
|
|
814
|
+
}
|
|
815
|
+
function isLinkUnavailable(link, map) {
|
|
816
|
+
if (!link || !map) return false;
|
|
817
|
+
if (link.remotePath && map[linkUnavailableKeyForPath(link.remotePath)]) return true;
|
|
818
|
+
if (link.expiredHref && map[linkUnavailableKeyForHref(link.expiredHref)]) return true;
|
|
819
|
+
if (link.href && map[linkUnavailableKeyForHref(link.href)]) return true;
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
765
822
|
function truncateLabelForDisplay(label) {
|
|
766
823
|
if (!label) return label;
|
|
767
824
|
if (label.length <= LINK_LABEL_MAX_DISPLAY_CHARS) return label;
|
|
@@ -790,15 +847,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
790
847
|
};
|
|
791
848
|
var apiReportedContextWindows = {};
|
|
792
849
|
var projectContextWindows = {};
|
|
793
|
-
function setProjectContextWindow(
|
|
794
|
-
var key = (
|
|
850
|
+
function setProjectContextWindow(projectId, tokens) {
|
|
851
|
+
var key = (projectId || "").trim();
|
|
795
852
|
if (!key) return;
|
|
796
853
|
var n = Number(tokens);
|
|
797
854
|
if (Number.isFinite(n) && n > 0) projectContextWindows[key] = Math.floor(n);
|
|
798
855
|
else delete projectContextWindows[key];
|
|
799
856
|
}
|
|
800
|
-
function getProjectContextWindow(
|
|
801
|
-
var key = (
|
|
857
|
+
function getProjectContextWindow(projectId) {
|
|
858
|
+
var key = (projectId || "").trim();
|
|
802
859
|
return key && projectContextWindows[key] ? projectContextWindows[key] : null;
|
|
803
860
|
}
|
|
804
861
|
var OUTPUT_TOKEN_RESERVE = 22e3;
|
|
@@ -815,8 +872,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
815
872
|
function estimateMessageTokens(msg) {
|
|
816
873
|
return estimateTextTokens(msg.content) + estimateTextTokens(msg.role) + 6;
|
|
817
874
|
}
|
|
818
|
-
function getContextWindow(platform, model,
|
|
819
|
-
var override =
|
|
875
|
+
function getContextWindow(platform, model, projectId) {
|
|
876
|
+
var override = projectId ? getProjectContextWindow(projectId) : null;
|
|
820
877
|
if (override) return override;
|
|
821
878
|
var normalized = (model || "").trim().toLowerCase();
|
|
822
879
|
if (normalized) {
|
|
@@ -835,12 +892,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
835
892
|
return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
|
|
836
893
|
}
|
|
837
894
|
function buildBoundedChatMessages(options) {
|
|
838
|
-
var contextWindow = getContextWindow(options.platform, options.model, options.
|
|
895
|
+
var contextWindow = getContextWindow(options.platform, options.model, options.projectId);
|
|
839
896
|
var contextBasedBudget = Math.max(
|
|
840
897
|
MIN_INPUT_TOKEN_BUDGET,
|
|
841
898
|
contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER
|
|
842
899
|
);
|
|
843
|
-
var scaled = !!(options.
|
|
900
|
+
var scaled = !!(options.projectId && getProjectContextWindow(options.projectId));
|
|
844
901
|
var claudeInputCap = scaled ? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO)) : CLAUDE_PER_REQUEST_INPUT_CAP;
|
|
845
902
|
var availableInputBudget = options.platform === "claude" ? Math.min(contextBasedBudget, claudeInputCap) : contextBasedBudget;
|
|
846
903
|
var systemCost = estimateTextTokens(options.systemPrompt) + 12;
|
|
@@ -852,7 +909,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
852
909
|
var trimmed = windowed.map(function(m, i2) {
|
|
853
910
|
if (i2 === latestIndex) return m;
|
|
854
911
|
var stripped = stripFileBlocksFromHistory(m.content);
|
|
855
|
-
var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.
|
|
912
|
+
var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.projectId, m.role !== "user");
|
|
856
913
|
return Object.assign({}, m, { content: sanitized });
|
|
857
914
|
});
|
|
858
915
|
var bounded = [], used = 0;
|
|
@@ -1029,6 +1086,141 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1029
1086
|
};
|
|
1030
1087
|
}
|
|
1031
1088
|
|
|
1089
|
+
// src/engine/link_markup.ts
|
|
1090
|
+
function escapeInlineHtml(v) {
|
|
1091
|
+
return String(v == null ? "" : v).replace(/[&<>"']/g, function(ch) {
|
|
1092
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
var IMAGE_PREVIEWS_PER_MESSAGE = 8;
|
|
1096
|
+
var INLINE_LINK_GLYPH = "\u2197";
|
|
1097
|
+
var INLINE_LINK_UNAVAILABLE_GLYPH = "\u2715";
|
|
1098
|
+
var INLINE_LINK_UNAVAILABLE_SUFFIX = " (unavailable)";
|
|
1099
|
+
function renderInlineLinkHtml(link, opts) {
|
|
1100
|
+
var o = opts || {};
|
|
1101
|
+
var unavailable = !!o.unavailable;
|
|
1102
|
+
var refreshing = !unavailable && !!o.refreshing;
|
|
1103
|
+
var full = link.fullLabel || link.label;
|
|
1104
|
+
var preview = !!link.image && !!link.remotePath && o.allowImagePreview !== false && !unavailable;
|
|
1105
|
+
var cls = ["bq-link-button"];
|
|
1106
|
+
if (link.expired) cls.push("is-expired");
|
|
1107
|
+
if (refreshing) cls.push("is-refreshing");
|
|
1108
|
+
if (unavailable) cls.push("is-unavailable");
|
|
1109
|
+
if (preview) cls.push("is-image-preview");
|
|
1110
|
+
var labelText = (unavailable ? INLINE_LINK_UNAVAILABLE_GLYPH : INLINE_LINK_GLYPH) + " " + link.label + (unavailable ? INLINE_LINK_UNAVAILABLE_SUFFIX : refreshing ? " (fetching...)" : "");
|
|
1111
|
+
var attrs = ['class="' + cls.join(" ") + '"'];
|
|
1112
|
+
if (unavailable) attrs.push('aria-disabled="true"', 'data-bq-unavailable="1"');
|
|
1113
|
+
else attrs.push('href="' + escapeInlineHtml(link.href) + '"', 'target="_blank"', 'rel="noopener noreferrer"');
|
|
1114
|
+
attrs.push('title="' + escapeInlineHtml(unavailable ? full + INLINE_LINK_UNAVAILABLE_SUFFIX : full) + '"');
|
|
1115
|
+
if (!preview && !unavailable) attrs.push('download="' + escapeInlineHtml(full) + '"');
|
|
1116
|
+
attrs.push('data-bq-link="1"');
|
|
1117
|
+
if (link.expired && !unavailable) attrs.push('data-bq-expired="1"');
|
|
1118
|
+
if (link.expiredHref) attrs.push('data-bq-expired-href="' + escapeInlineHtml(link.expiredHref) + '"');
|
|
1119
|
+
if (link.remotePath) attrs.push('data-bq-remote-path="' + escapeInlineHtml(link.remotePath) + '"');
|
|
1120
|
+
if (link.fullLabel) attrs.push('data-bq-full-label="' + escapeInlineHtml(link.fullLabel) + '"');
|
|
1121
|
+
if (!preview) return "<a " + attrs.join(" ") + ">" + escapeInlineHtml(labelText) + "</a>";
|
|
1122
|
+
return "<a " + attrs.join(" ") + '><img class="bq-img-preview" alt="' + escapeInlineHtml(full) + '" data-bq-img-path="' + escapeInlineHtml(link.remotePath || "") + '" data-bq-img-type="' + escapeInlineHtml(link.image ? link.image.contentType : "") + '" loading="lazy" decoding="async"><span class="bq-loader" data-bq-img-loader="1"></span><span class="bq-img-preview-caption" translate="no">' + escapeInlineHtml(labelText) + "</span></a>";
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/engine/image_preview.ts
|
|
1126
|
+
var previewUrlCache = /* @__PURE__ */ Object.create(null);
|
|
1127
|
+
var previewInFlight = /* @__PURE__ */ Object.create(null);
|
|
1128
|
+
function cacheKey(scope, path) {
|
|
1129
|
+
return scope + "\0" + path;
|
|
1130
|
+
}
|
|
1131
|
+
function clearImagePreviewCache(scope) {
|
|
1132
|
+
var prefix = scope + "\0";
|
|
1133
|
+
for (var k in previewUrlCache) if (k.indexOf(prefix) === 0) delete previewUrlCache[k];
|
|
1134
|
+
for (var f in previewInFlight) if (f.indexOf(prefix) === 0) delete previewInFlight[f];
|
|
1135
|
+
for (var s in staleImagePreviews) if (s.indexOf(prefix) === 0) delete staleImagePreviews[s];
|
|
1136
|
+
}
|
|
1137
|
+
function peekImagePreviewUrl(ctx, remotePath) {
|
|
1138
|
+
var hit = previewUrlCache[cacheKey(ctx.scope, remotePath)];
|
|
1139
|
+
if (hit && Date.now() - hit.at < LINK_REFRESH_WINDOW_MS) return hit.url;
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
function resolveImagePreviewUrl(ctx, remotePath, contentType, refresh) {
|
|
1143
|
+
var key = cacheKey(ctx.scope, remotePath);
|
|
1144
|
+
if (staleImagePreviews[key]) {
|
|
1145
|
+
delete staleImagePreviews[key];
|
|
1146
|
+
refresh = true;
|
|
1147
|
+
}
|
|
1148
|
+
if (refresh) {
|
|
1149
|
+
delete previewUrlCache[key];
|
|
1150
|
+
delete previewInFlight[key];
|
|
1151
|
+
} else {
|
|
1152
|
+
var warm = peekImagePreviewUrl(ctx, remotePath);
|
|
1153
|
+
if (warm) return Promise.resolve(warm);
|
|
1154
|
+
var flight = previewInFlight[key];
|
|
1155
|
+
if (flight) return flight;
|
|
1156
|
+
}
|
|
1157
|
+
var run = ctx.mint(remotePath, contentType, refresh).then(function(url) {
|
|
1158
|
+
if (previewInFlight[key] === run) {
|
|
1159
|
+
previewUrlCache[key] = { url, at: Date.now() };
|
|
1160
|
+
delete previewInFlight[key];
|
|
1161
|
+
}
|
|
1162
|
+
return url;
|
|
1163
|
+
}, function(e) {
|
|
1164
|
+
if (previewInFlight[key] === run) delete previewInFlight[key];
|
|
1165
|
+
throw e;
|
|
1166
|
+
});
|
|
1167
|
+
previewInFlight[key] = run;
|
|
1168
|
+
return run;
|
|
1169
|
+
}
|
|
1170
|
+
function markImagePreviewStale(scope, remotePath) {
|
|
1171
|
+
if (!remotePath) return;
|
|
1172
|
+
staleImagePreviews[cacheKey(scope, remotePath)] = true;
|
|
1173
|
+
delete previewUrlCache[cacheKey(scope, remotePath)];
|
|
1174
|
+
}
|
|
1175
|
+
var staleImagePreviews = /* @__PURE__ */ Object.create(null);
|
|
1176
|
+
function hydrateImagePreviews(imgs, ctx) {
|
|
1177
|
+
for (var i = 0; i < imgs.length; i++) hydrateOne(imgs[i], ctx);
|
|
1178
|
+
}
|
|
1179
|
+
function hydrateOne(img, ctx) {
|
|
1180
|
+
if (img.getAttribute("data-bq-img-state")) return;
|
|
1181
|
+
var path = img.getAttribute("data-bq-img-path");
|
|
1182
|
+
var type = img.getAttribute("data-bq-img-type") || "";
|
|
1183
|
+
if (!path) {
|
|
1184
|
+
img.setAttribute("data-bq-img-state", "error");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
img.setAttribute("data-bq-img-state", "loading");
|
|
1188
|
+
img.addEventListener("load", function() {
|
|
1189
|
+
img.setAttribute("data-bq-img-state", "ready");
|
|
1190
|
+
if (ctx.onLoad) ctx.onLoad(path);
|
|
1191
|
+
});
|
|
1192
|
+
img.addEventListener("error", function() {
|
|
1193
|
+
onImageError(img, ctx, path, type);
|
|
1194
|
+
});
|
|
1195
|
+
var warm = peekImagePreviewUrl(ctx, path);
|
|
1196
|
+
if (warm) {
|
|
1197
|
+
img.setAttribute("src", warm);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
resolveImagePreviewUrl(ctx, path, type).then(function(url) {
|
|
1201
|
+
if (img.getAttribute("data-bq-img-state") !== "loading") return;
|
|
1202
|
+
img.setAttribute("src", url);
|
|
1203
|
+
}, function(e) {
|
|
1204
|
+
img.setAttribute("data-bq-img-state", "error");
|
|
1205
|
+
if (ctx.onError) ctx.onError(path, e);
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
function onImageError(img, ctx, path, type) {
|
|
1209
|
+
if (img.getAttribute("data-bq-img-retry") === "1") {
|
|
1210
|
+
img.setAttribute("data-bq-img-state", "error");
|
|
1211
|
+
if (ctx.onError) ctx.onError(path, new Error("image preview failed to load"));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
img.setAttribute("data-bq-img-retry", "1");
|
|
1215
|
+
img.removeAttribute("src");
|
|
1216
|
+
resolveImagePreviewUrl(ctx, path, type, true).then(function(url) {
|
|
1217
|
+
img.setAttribute("src", url);
|
|
1218
|
+
}, function(e) {
|
|
1219
|
+
img.setAttribute("data-bq-img-state", "error");
|
|
1220
|
+
if (ctx.onError) ctx.onError(path, e);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1032
1224
|
// src/engine/time.ts
|
|
1033
1225
|
function wallClockNow() {
|
|
1034
1226
|
return Date.now();
|
|
@@ -1092,6 +1284,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1092
1284
|
var DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
|
|
1093
1285
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
1094
1286
|
var clientSecretRequest = (opts) => chatEngineConfig().clientSecretRequest(opts);
|
|
1287
|
+
var VARIANT_IMAGE_DETAIL = "original";
|
|
1288
|
+
var VARIANT_TEXT_VERBOSITY = "high";
|
|
1289
|
+
var OLDEST_NANO_REASONING_EFFORT = "high";
|
|
1290
|
+
var isOpenAINano = (model) => {
|
|
1291
|
+
const normalized = (model).trim().toLowerCase();
|
|
1292
|
+
if (!/(^|-)nano(-|$)/.test(normalized)) return false;
|
|
1293
|
+
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
|
|
1294
|
+
if (!match) return false;
|
|
1295
|
+
const major = Number(match[1]);
|
|
1296
|
+
const minor = match[2] === void 0 ? null : Number(match[2]);
|
|
1297
|
+
return major > 5 || major === 5 && minor !== null && minor >= 4;
|
|
1298
|
+
};
|
|
1299
|
+
var variantIndexingOptions = (model) => {
|
|
1300
|
+
if (!isOpenAINano(model) || !isOldestNano(model)) return {};
|
|
1301
|
+
return {
|
|
1302
|
+
...{ text: { verbosity: VARIANT_TEXT_VERBOSITY } } ,
|
|
1303
|
+
...{ reasoning: { effort: OLDEST_NANO_REASONING_EFFORT } }
|
|
1304
|
+
};
|
|
1305
|
+
};
|
|
1095
1306
|
var getOpenAIImageDetail = (model) => {
|
|
1096
1307
|
const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
|
|
1097
1308
|
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
|
|
@@ -1105,12 +1316,40 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1105
1316
|
if (!supportsOriginal) {
|
|
1106
1317
|
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
1107
1318
|
}
|
|
1108
|
-
return isVariant ?
|
|
1319
|
+
return isVariant ? VARIANT_IMAGE_DETAIL : "original";
|
|
1109
1320
|
};
|
|
1110
1321
|
var getRenderImageDetail = (model) => {
|
|
1111
1322
|
const detail = getOpenAIImageDetail(model);
|
|
1112
1323
|
return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? "high" : detail;
|
|
1113
1324
|
};
|
|
1325
|
+
var OPENAI_VERSIONED_ID = /^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/;
|
|
1326
|
+
var isRecognisedOpenAIVersion = (model) => OPENAI_VERSIONED_ID.test((model).trim().toLowerCase());
|
|
1327
|
+
var isOldestNano = (model) => {
|
|
1328
|
+
const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
|
|
1329
|
+
if (!/(^|-)nano(-|$)/.test(normalized)) return false;
|
|
1330
|
+
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
|
|
1331
|
+
if (!match) return true;
|
|
1332
|
+
const major = Number(match[1]);
|
|
1333
|
+
const minor = match[2] === void 0 ? null : Number(match[2]);
|
|
1334
|
+
if (major < 5) return true;
|
|
1335
|
+
if (major > 5) return false;
|
|
1336
|
+
return minor === null || minor <= 4;
|
|
1337
|
+
};
|
|
1338
|
+
var SMALL_TIER_PAGES_PER_WINDOW = 2;
|
|
1339
|
+
var DOWNSAMPLED_TIER_TILE = 2;
|
|
1340
|
+
function getVisionProfile(model) {
|
|
1341
|
+
const detail = getRenderImageDetail(model);
|
|
1342
|
+
if (!isRecognisedOpenAIVersion(model)) {
|
|
1343
|
+
return { detail, pagesPerWindow: RENDER_PAGES_PER_WINDOW, tile: 1 };
|
|
1344
|
+
}
|
|
1345
|
+
if (detail !== "original") {
|
|
1346
|
+
return { detail, pagesPerWindow: SMALL_TIER_PAGES_PER_WINDOW, tile: DOWNSAMPLED_TIER_TILE };
|
|
1347
|
+
}
|
|
1348
|
+
if (isOldestNano(model)) {
|
|
1349
|
+
return { detail, pagesPerWindow: SMALL_TIER_PAGES_PER_WINDOW, tile: 1 };
|
|
1350
|
+
}
|
|
1351
|
+
return { detail, pagesPerWindow: RENDER_PAGES_PER_WINDOW, tile: 1 };
|
|
1352
|
+
}
|
|
1114
1353
|
var IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"'()\[\]]+?\.(?:jpg|jpeg|png|gif|webp)(?:\?[^\s<>"'()\[\]]*)?/gi;
|
|
1115
1354
|
function transformContentWithImages(content) {
|
|
1116
1355
|
if (typeof content !== "string" || !content) {
|
|
@@ -1191,6 +1430,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1191
1430
|
});
|
|
1192
1431
|
}
|
|
1193
1432
|
var POLL_INTERVAL = 3e3;
|
|
1433
|
+
var MAX_CONCURRENT_BG_POLLS = 6;
|
|
1194
1434
|
async function callClaudeWithMcp({
|
|
1195
1435
|
prompt,
|
|
1196
1436
|
messages,
|
|
@@ -1355,16 +1595,18 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1355
1595
|
const renderFrom = Math.max(0, info.renderFrom || 0);
|
|
1356
1596
|
const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
|
|
1357
1597
|
const renderDetail = platform === "openai" ? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL) : void 0;
|
|
1598
|
+
const visionProfile = platform === "openai" ? getVisionProfile(info.model || DEFAULT_OPENAI_MODEL) : { pagesPerWindow: RENDER_PAGES_PER_WINDOW, tile: 1 };
|
|
1358
1599
|
const skapiRender = visionFile && renderPlaceholder ? {
|
|
1359
1600
|
_skapi_render: [
|
|
1360
1601
|
{
|
|
1361
1602
|
path: attachment.storagePath,
|
|
1362
1603
|
from: renderFrom,
|
|
1363
|
-
count:
|
|
1604
|
+
count: visionProfile.pagesPerWindow,
|
|
1364
1605
|
placeholder: renderPlaceholder,
|
|
1365
1606
|
name: attachment.name,
|
|
1366
1607
|
mime: attachment.mime,
|
|
1367
1608
|
detail: renderDetail,
|
|
1609
|
+
tile: visionProfile.tile,
|
|
1368
1610
|
auto_continue: true,
|
|
1369
1611
|
continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder)
|
|
1370
1612
|
}
|
|
@@ -1381,6 +1623,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1381
1623
|
name: attachment.name,
|
|
1382
1624
|
mime: attachment.mime,
|
|
1383
1625
|
kind: "window",
|
|
1626
|
+
// Same per-image `detail` the render path sends. Without it the worker falls
|
|
1627
|
+
// back to its model-blind default of 'high', so a spreadsheet's embedded
|
|
1628
|
+
// photos were tiled at lower resolution than the SAME model gets for a PDF
|
|
1629
|
+
// page or a chat attachment. That is why a model could describe an attached
|
|
1630
|
+
// photo but reported the pictures inside a sheet as only partly legible.
|
|
1631
|
+
detail: renderDetail,
|
|
1384
1632
|
auto_continue: true,
|
|
1385
1633
|
continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true)
|
|
1386
1634
|
}
|
|
@@ -1390,13 +1638,27 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1390
1638
|
const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
1391
1639
|
const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
|
|
1392
1640
|
const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
|
|
1393
|
-
const skapiExtract = extractContent && extractContent.length ? {
|
|
1641
|
+
const skapiExtract = extractContent && extractContent.length ? {
|
|
1642
|
+
_skapi_extract: extractContent.map((d) => ({
|
|
1643
|
+
...d,
|
|
1644
|
+
// FIRST pass of an INDEXING run only: tells the worker to also pull the
|
|
1645
|
+
// file's embedded pictures into __MEDIA__ and register their records.
|
|
1646
|
+
// Chat-turn extraction (callClaudeWithMcp / callOpenAIWithPublicMcp)
|
|
1647
|
+
// never sets this, so merely ATTACHING a file to a chat message cannot
|
|
1648
|
+
// write media records; a CONTINUE pass skips it because the first pass
|
|
1649
|
+
// already saved (the save is whole-file, not windowed).
|
|
1650
|
+
save_media: !continuing
|
|
1651
|
+
}))
|
|
1652
|
+
} : {};
|
|
1394
1653
|
const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : windowedRead && windowPlaceholder ? buildIndexingWindowMessage(attachment, windowPlaceholder, false) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
|
|
1395
1654
|
attachment,
|
|
1396
1655
|
parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
|
|
1397
1656
|
);
|
|
1398
1657
|
const systemPrompt = buildIndexingSystemPrompt({
|
|
1399
|
-
|
|
1658
|
+
// The model copies this id verbatim into project_id tool calls, so it must be
|
|
1659
|
+
// the PUBLIC token whenever the host supplied one; the raw code is rejected
|
|
1660
|
+
// by the tools' schema pattern.
|
|
1661
|
+
projectId: info.publicProjectId || service,
|
|
1400
1662
|
serviceName: info.serviceName,
|
|
1401
1663
|
serviceDescription: info.serviceDescription
|
|
1402
1664
|
});
|
|
@@ -1418,6 +1680,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1418
1680
|
data: {
|
|
1419
1681
|
model: resolvedModel2,
|
|
1420
1682
|
max_output_tokens: MAX_TOKENS,
|
|
1683
|
+
// Nano-only transcription knobs. Indexing only; see variantIndexingOptions.
|
|
1684
|
+
...variantIndexingOptions(resolvedModel2),
|
|
1421
1685
|
...skapiExtract,
|
|
1422
1686
|
...skapiRender,
|
|
1423
1687
|
...skapiWindow,
|
|
@@ -1549,7 +1813,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1549
1813
|
return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
|
|
1550
1814
|
}
|
|
1551
1815
|
var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
|
|
1816
|
+
var EMPTY_INDEXING_REPLY = "Finished reading this file.";
|
|
1552
1817
|
var MAX_INDEXING_RESUME_PASSES = 6;
|
|
1818
|
+
var CHAT_HISTORY_PAGE_LIMIT = 500;
|
|
1553
1819
|
async function getChatHistory(params, fetchOptions) {
|
|
1554
1820
|
const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
1555
1821
|
const p = Object.assign(
|
|
@@ -1563,7 +1829,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1563
1829
|
);
|
|
1564
1830
|
return chatEngineConfig().clientSecretRequestHistory(
|
|
1565
1831
|
p,
|
|
1566
|
-
Object.assign({ ascending: false }, fetchOptions)
|
|
1832
|
+
Object.assign({ ascending: false, limit: CHAT_HISTORY_PAGE_LIMIT }, fetchOptions)
|
|
1567
1833
|
);
|
|
1568
1834
|
}
|
|
1569
1835
|
|
|
@@ -1630,6 +1896,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1630
1896
|
var userText = extractLastUserTextFromRequest(requestBody);
|
|
1631
1897
|
var assistantText = isPending ? "" : (extractAssistantText(response) || "").trim() || "";
|
|
1632
1898
|
var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
|
|
1899
|
+
var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && !!assistantText && assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
|
|
1900
|
+
if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
1633
1901
|
var serverItemId = item && typeof item.id === "string" && item.id ? item.id : void 0;
|
|
1634
1902
|
var createdTs = Number(item && item.created);
|
|
1635
1903
|
var updatedTs = Number(item && item.updated);
|
|
@@ -1654,7 +1922,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1654
1922
|
displayContent = userText;
|
|
1655
1923
|
}
|
|
1656
1924
|
} else {
|
|
1657
|
-
displayContent = sanitizeAttachmentLinksForHistory(userText, opts.
|
|
1925
|
+
displayContent = sanitizeAttachmentLinksForHistory(userText, opts.projectId);
|
|
1658
1926
|
}
|
|
1659
1927
|
var userMsg = { role: "user", content: displayContent };
|
|
1660
1928
|
if (isInProcess) userMsg.isPendingInProcess = true;
|
|
@@ -1681,11 +1949,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1681
1949
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
1682
1950
|
if (replyTs !== void 0) em._ts = replyTs;
|
|
1683
1951
|
mapped.push(em);
|
|
1684
|
-
} else if (assistantText) {
|
|
1685
|
-
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.
|
|
1952
|
+
} else if (assistantText || reportedComplete) {
|
|
1953
|
+
var okm = { role: "assistant", content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
|
|
1686
1954
|
if (item._isBgTask) okm.isBackgroundTask = true;
|
|
1687
1955
|
if (serverItemId !== void 0) okm._serverItemId = serverItemId;
|
|
1688
1956
|
if (replyTs !== void 0) okm._ts = replyTs;
|
|
1957
|
+
if (reportedComplete) okm._indexComplete = true;
|
|
1689
1958
|
mapped.push(okm);
|
|
1690
1959
|
}
|
|
1691
1960
|
});
|
|
@@ -1744,6 +2013,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1744
2013
|
function createHistoryFiller(base) {
|
|
1745
2014
|
var pending = [];
|
|
1746
2015
|
var running = false;
|
|
2016
|
+
var fetching = false;
|
|
2017
|
+
function announce(next) {
|
|
2018
|
+
if (fetching === next) return;
|
|
2019
|
+
fetching = next;
|
|
2020
|
+
if (!base.onRunningChange) return;
|
|
2021
|
+
try {
|
|
2022
|
+
base.onRunningChange(next);
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1747
2026
|
async function allSatisfied() {
|
|
1748
2027
|
var next = [];
|
|
1749
2028
|
for (var i = 0; i < pending.length; i++) {
|
|
@@ -1753,23 +2032,33 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1753
2032
|
return pending.length === 0;
|
|
1754
2033
|
}
|
|
1755
2034
|
return {
|
|
2035
|
+
// The published fact, so a view and `isRunning()` can never disagree about
|
|
2036
|
+
// what they are showing. A fill that never fetches is not something anyone
|
|
2037
|
+
// outside this module has any use for knowing about.
|
|
1756
2038
|
isRunning: function() {
|
|
1757
|
-
return
|
|
2039
|
+
return fetching;
|
|
1758
2040
|
},
|
|
1759
2041
|
fill: function(isSatisfied) {
|
|
1760
2042
|
pending.push(isSatisfied);
|
|
1761
2043
|
if (running) return Promise.resolve();
|
|
1762
2044
|
running = true;
|
|
1763
2045
|
var done = function() {
|
|
1764
|
-
running = false;
|
|
1765
2046
|
pending = [];
|
|
2047
|
+
running = false;
|
|
2048
|
+
announce(false);
|
|
1766
2049
|
};
|
|
1767
2050
|
return fillHistoryViewport({
|
|
1768
2051
|
isSatisfied: allSatisfied,
|
|
1769
2052
|
isEndOfList: base.isEndOfList,
|
|
1770
2053
|
isLoading: base.isLoading,
|
|
1771
2054
|
messageCount: base.messageCount,
|
|
1772
|
-
|
|
2055
|
+
// The span opens HERE, at the first real page request: past
|
|
2056
|
+
// isEndOfList, past isStale, past isSatisfied. Everything before this
|
|
2057
|
+
// point is a fill that concluded there was nothing to do.
|
|
2058
|
+
fetchOlder: function() {
|
|
2059
|
+
announce(true);
|
|
2060
|
+
return base.fetchOlder();
|
|
2061
|
+
},
|
|
1773
2062
|
isStale: base.isStale,
|
|
1774
2063
|
maxPages: base.maxPages
|
|
1775
2064
|
}).then(done, done);
|
|
@@ -1779,12 +2068,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1779
2068
|
|
|
1780
2069
|
// src/engine/session.ts
|
|
1781
2070
|
var WORKER_PASS_ADOPT_LIMIT = 20;
|
|
2071
|
+
var LIVE_INDEX_SNAPSHOT_MAX_AGE_MS = 5e3;
|
|
2072
|
+
var INDEX_DISPATCH_CLAIM_MS = 2 * 60 * 1e3;
|
|
1782
2073
|
var WORKER_PASS_ADOPT_ATTEMPTS = [0, 2e3, 6e3];
|
|
1783
2074
|
var INDEXING_DRAIN_BUSY_POLL_MS = 8e3;
|
|
1784
2075
|
var INDEXING_DRAIN_CONFIRM_POLL_MS = 3e3;
|
|
1785
2076
|
var INDEXING_DRAIN_IDLE_LOOKS = 2;
|
|
1786
2077
|
var INDEXING_DRAIN_MIN_MS = 8e3;
|
|
1787
2078
|
var INDEXING_DRAIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
2079
|
+
var INDEXING_DRAIN_LOOK_TIMEOUT_MS = 45e3;
|
|
2080
|
+
var INDEXING_DRAIN_NUDGE_MIN_GAP_MS = 1500;
|
|
1788
2081
|
var _g = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1789
2082
|
function nowMs() {
|
|
1790
2083
|
return _g.performance && typeof _g.performance.now === "function" ? _g.performance.now() : Date.now();
|
|
@@ -1846,7 +2139,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1846
2139
|
historyEndOfList: false,
|
|
1847
2140
|
historyStartKeyHistory: [],
|
|
1848
2141
|
historyRequestToken: 0,
|
|
1849
|
-
gateRefreshToken: 0
|
|
2142
|
+
gateRefreshToken: 0,
|
|
2143
|
+
liveIndexKeys: {},
|
|
2144
|
+
liveIndexChecked: false,
|
|
2145
|
+
stoppedIndexIds: {}
|
|
1850
2146
|
};
|
|
1851
2147
|
this.bgTaskQueue = [];
|
|
1852
2148
|
this.cancelledServerIds = /* @__PURE__ */ new Set();
|
|
@@ -1860,6 +2156,254 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1860
2156
|
this._stageSeq = 0;
|
|
1861
2157
|
this._uploadBatches = 0;
|
|
1862
2158
|
this._indexDispatchesInFlight = 0;
|
|
2159
|
+
this._drainNudges = [];
|
|
2160
|
+
this._liveStages = {};
|
|
2161
|
+
this._liveIndexKey = "";
|
|
2162
|
+
this._liveIndexAt = 0;
|
|
2163
|
+
this._indexClaims = {};
|
|
2164
|
+
}
|
|
2165
|
+
/** What the display layer needs to decide whether a run is finished. `keys` holds
|
|
2166
|
+
* every file the server still has indexing work for; `checked` is false until the
|
|
2167
|
+
* first answer for this chat, and false means "we do not know yet". */
|
|
2168
|
+
getLiveIndexState() {
|
|
2169
|
+
return { keys: this.state.liveIndexKeys, checked: this.state.liveIndexChecked };
|
|
2170
|
+
}
|
|
2171
|
+
/** Passes that were on a row when the user stopped it, so the display layer can
|
|
2172
|
+
* still tell that this run was stopped once the stop has left no other trace.
|
|
2173
|
+
* See cancelIndexingGroup, which fills it, and buildChatDisplayList, which is
|
|
2174
|
+
* the only reader. */
|
|
2175
|
+
getStoppedIndexIds() {
|
|
2176
|
+
return this.state.stoppedIndexIds;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Is this file ALREADY being indexed by this client?
|
|
2180
|
+
*
|
|
2181
|
+
* One live run per file, and the reason is what a second one looks like: the
|
|
2182
|
+
* conversation grows a SECOND collapsed row for the same file (a run is opened
|
|
2183
|
+
* by every FIRST pass, so two of them are two rows), the same document is read
|
|
2184
|
+
* twice at full provider cost, and the two chains fight over the same records —
|
|
2185
|
+
* the delete-then-repost that starts run 2 wipes what run 1 has saved so far.
|
|
2186
|
+
*
|
|
2187
|
+
* Asked of this client's own live work, so it cannot be wrong in the dangerous
|
|
2188
|
+
* direction: a queued/running pass keeps its bgTaskQueue entry until its bubble
|
|
2189
|
+
* settles, and a settled run answers false, which is what a genuine later
|
|
2190
|
+
* re-index needs.
|
|
2191
|
+
*
|
|
2192
|
+
* The retry that made this necessary: a chip whose INDEX request failed is
|
|
2193
|
+
* handed back to the composer to be retried on the next send, and an index
|
|
2194
|
+
* request can fail from the client's side (a lost ack, an expired token on the
|
|
2195
|
+
* response) while the server has already queued the pass. The retry then indexes
|
|
2196
|
+
* a file that was never not being indexed.
|
|
2197
|
+
*/
|
|
2198
|
+
hasLiveIndexRun(storagePath) {
|
|
2199
|
+
if (!storagePath) return false;
|
|
2200
|
+
var claimed = this._indexClaims[this._indexClaimKey(storagePath)];
|
|
2201
|
+
if (claimed && nowMs() - claimed < INDEX_DISPATCH_CLAIM_MS) return true;
|
|
2202
|
+
var id = this.host.getIdentity();
|
|
2203
|
+
for (var i = 0; i < this.bgTaskQueue.length; i++) {
|
|
2204
|
+
var e = this.bgTaskQueue[i];
|
|
2205
|
+
if (e && e.storagePath === storagePath && e.projectId === id.projectId && e.platform === id.platform) return true;
|
|
2206
|
+
}
|
|
2207
|
+
return this.state.messages.some(function(m) {
|
|
2208
|
+
if (!m.isBackgroundTask || m.role !== "user" || m.isCancelled) return false;
|
|
2209
|
+
if (!(m.isPendingQueued || m.isPendingInProcess || m.isSendingToServer)) return false;
|
|
2210
|
+
return !!m._indexFile && m._indexFile.path === storagePath;
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
/** Storage paths are project-relative, and one ChatSession serves every
|
|
2214
|
+
* project, so a claim has to be scoped the way a stop is (_indexKeyOf). */
|
|
2215
|
+
_indexClaimKey(storagePath) {
|
|
2216
|
+
return this.getHistoryCacheKey() + "|" + storagePath;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Take this file's indexing slot, or report that someone already has it.
|
|
2220
|
+
*
|
|
2221
|
+
* The check-and-CLAIM is what makes it safe against a second caller arriving
|
|
2222
|
+
* mid-flight: the claim is written SYNCHRONOUSLY, before the first await, so a
|
|
2223
|
+
* concurrent caller sees it even though no request has completed and no queue
|
|
2224
|
+
* has admitted anything. Ask-then-dispatch could not do that — every source it
|
|
2225
|
+
* consults only learns about a dispatch after the ack.
|
|
2226
|
+
*
|
|
2227
|
+
* Returns true when the caller owns the slot and should dispatch. A caller that
|
|
2228
|
+
* then fails to dispatch MUST releaseIndexRun, or the file waits out the claim
|
|
2229
|
+
* (a few minutes) before it can be retried.
|
|
2230
|
+
*/
|
|
2231
|
+
claimIndexRun(storagePath) {
|
|
2232
|
+
var self = this;
|
|
2233
|
+
if (!storagePath) return Promise.resolve(true);
|
|
2234
|
+
if (this.hasLiveIndexRun(storagePath)) return Promise.resolve(false);
|
|
2235
|
+
this._indexClaims[this._indexClaimKey(storagePath)] = nowMs();
|
|
2236
|
+
return this._refreshLiveIndexKeys(LIVE_INDEX_SNAPSHOT_MAX_AGE_MS).then(function() {
|
|
2237
|
+
if (!self.state.liveIndexKeys[storagePath]) return true;
|
|
2238
|
+
self.releaseIndexRun(storagePath);
|
|
2239
|
+
return false;
|
|
2240
|
+
}).catch(function() {
|
|
2241
|
+
return true;
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
/** Give the slot back — the dispatch failed, or was abandoned. */
|
|
2245
|
+
releaseIndexRun(storagePath) {
|
|
2246
|
+
if (storagePath) delete this._indexClaims[this._indexClaimKey(storagePath)];
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* The same question, asked of the SERVER when this page cannot answer it.
|
|
2250
|
+
*
|
|
2251
|
+
* hasLiveIndexRun only knows what this page did. That is not enough for the
|
|
2252
|
+
* case duplicates actually come from: the first run was started before a
|
|
2253
|
+
* reload, or in another tab, or its bubble has since been paged out of the
|
|
2254
|
+
* loaded window — and then the retry finds nothing locally and starts a second
|
|
2255
|
+
* run of a file that is still being indexed. The queue is the one place that
|
|
2256
|
+
* knows, and it is already asked for exactly this list.
|
|
2257
|
+
*
|
|
2258
|
+
* Only a POSITIVE answer is used. Absence proves nothing here (the query is
|
|
2259
|
+
* capped, and `liveIndexChecked` records that), so an unanswerable question
|
|
2260
|
+
* falls back to dispatching — the cost of a wrong "no" is the duplicate this
|
|
2261
|
+
* exists to prevent, and the cost of a wrong "yes" is a file that never gets
|
|
2262
|
+
* indexed at all. Only one of those is recoverable by the user.
|
|
2263
|
+
*/
|
|
2264
|
+
isIndexRunLive(storagePath) {
|
|
2265
|
+
var self = this;
|
|
2266
|
+
if (!storagePath) return Promise.resolve(false);
|
|
2267
|
+
if (this.hasLiveIndexRun(storagePath)) return Promise.resolve(true);
|
|
2268
|
+
return this._refreshLiveIndexKeys(LIVE_INDEX_SNAPSHOT_MAX_AGE_MS).then(function() {
|
|
2269
|
+
return !!self.state.liveIndexKeys[storagePath];
|
|
2270
|
+
}).catch(function() {
|
|
2271
|
+
return false;
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
/** Re-ask the queue which files are still being indexed, unless the answer we
|
|
2275
|
+
* have is younger than `maxAgeMs`. Shared by every caller that needs a current
|
|
2276
|
+
* one; the display layer's own refresh path is the adopt ladder. */
|
|
2277
|
+
_refreshLiveIndexKeys(maxAgeMs) {
|
|
2278
|
+
var self = this;
|
|
2279
|
+
var id = this.host.getIdentity();
|
|
2280
|
+
var platform = id.platform;
|
|
2281
|
+
if (!id.projectId || platform !== "claude" && platform !== "openai") return Promise.resolve();
|
|
2282
|
+
var askedKey = this.getHistoryCacheKey();
|
|
2283
|
+
if (this._liveIndexKey === askedKey && nowMs() - this._liveIndexAt < maxAgeMs) {
|
|
2284
|
+
return Promise.resolve();
|
|
2285
|
+
}
|
|
2286
|
+
var queue = bgIndexingQueueName(id.userId, id.projectId);
|
|
2287
|
+
var ask = function(status) {
|
|
2288
|
+
return Promise.resolve(getChatHistory(
|
|
2289
|
+
{ service: id.projectId, owner: id.owner, platform, queue, status },
|
|
2290
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2291
|
+
)).catch(function() {
|
|
2292
|
+
return null;
|
|
2293
|
+
});
|
|
2294
|
+
};
|
|
2295
|
+
return Promise.all([ask("pending"), ask("running")]).then(function(results) {
|
|
2296
|
+
if (results[0] === null || results[1] === null) return;
|
|
2297
|
+
if (self.getHistoryCacheKey() !== askedKey) return;
|
|
2298
|
+
self._liveIndexKey = askedKey;
|
|
2299
|
+
self._recordLiveIndexKeys(results);
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Replace the live-index snapshot from a queue query's raw items.
|
|
2304
|
+
*
|
|
2305
|
+
* Whole-snapshot, never incremental: the query returns everything unresolved on
|
|
2306
|
+
* the queue, so a file MISSING from it is precisely the fact we are after. Merging
|
|
2307
|
+
* would make a finished file impossible to observe.
|
|
2308
|
+
*/
|
|
2309
|
+
_recordLiveIndexKeys(lists) {
|
|
2310
|
+
var next = {};
|
|
2311
|
+
var truncated = false;
|
|
2312
|
+
var settledIds = {};
|
|
2313
|
+
this.state.messages.forEach(function(m) {
|
|
2314
|
+
if (!m._serverItemId) return;
|
|
2315
|
+
if (m.isPending || m.isPendingInProcess || m.isPendingQueued) return;
|
|
2316
|
+
settledIds[m._serverItemId] = true;
|
|
2317
|
+
});
|
|
2318
|
+
for (var li = 0; li < lists.length; li++) {
|
|
2319
|
+
var list = lists[li] && Array.isArray(lists[li].list) ? lists[li].list : [];
|
|
2320
|
+
if (list.length >= WORKER_PASS_ADOPT_LIMIT) truncated = true;
|
|
2321
|
+
for (var i = 0; i < list.length; i++) {
|
|
2322
|
+
var item = list[i];
|
|
2323
|
+
if (!item || item.status !== "pending" && item.status !== "running") continue;
|
|
2324
|
+
if (item.id && settledIds[item.id]) continue;
|
|
2325
|
+
var text = extractLastUserTextFromRequest(item.request_body);
|
|
2326
|
+
if (!isIndexingRequestText(text)) continue;
|
|
2327
|
+
var ref = parseIndexingRequestText(text);
|
|
2328
|
+
if (!ref) continue;
|
|
2329
|
+
if (ref.path) next[ref.path] = true;
|
|
2330
|
+
if (ref.name) next[ref.name] = true;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
var nowChecked = !truncated;
|
|
2334
|
+
var was = this.state.liveIndexKeys, changed = this.state.liveIndexChecked !== nowChecked;
|
|
2335
|
+
if (!changed) {
|
|
2336
|
+
for (var k in next) if (!was[k]) {
|
|
2337
|
+
changed = true;
|
|
2338
|
+
break;
|
|
2339
|
+
}
|
|
2340
|
+
if (!changed) {
|
|
2341
|
+
for (var k2 in was) if (!next[k2]) {
|
|
2342
|
+
changed = true;
|
|
2343
|
+
break;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
this.state.liveIndexKeys = next;
|
|
2348
|
+
this.state.liveIndexChecked = nowChecked;
|
|
2349
|
+
this._liveIndexAt = nowMs();
|
|
2350
|
+
this._liveIndexKey = this.getHistoryCacheKey();
|
|
2351
|
+
if (changed) this.host.notify();
|
|
2352
|
+
}
|
|
2353
|
+
/** Forget the snapshot: it describes ONE chat's queue, and the answer for the
|
|
2354
|
+
* project the user just switched to is unknown until it is asked for again. */
|
|
2355
|
+
_resetLiveIndexKeys() {
|
|
2356
|
+
this.state.liveIndexKeys = {};
|
|
2357
|
+
this.state.liveIndexChecked = false;
|
|
2358
|
+
this._liveIndexAt = 0;
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Ask the queue what is still indexing, once, for the chat that is on screen.
|
|
2362
|
+
*
|
|
2363
|
+
* Seeds the snapshot on a history load. Without it a reloaded chat has no way to
|
|
2364
|
+
* learn that a run it can see is over: the adopt ladder that normally answers this
|
|
2365
|
+
* only fires when a pass SETTLES, and after a reload there is no pass left to
|
|
2366
|
+
* settle — so every finished worker-driven row would spin forever.
|
|
2367
|
+
*
|
|
2368
|
+
* Best-effort: a failure leaves `checked` false, which reads as "still working"
|
|
2369
|
+
* rather than as a false all-clear.
|
|
2370
|
+
*
|
|
2371
|
+
* Delegates to the adopt ladder rather than asking once. A single empty look is
|
|
2372
|
+
* exactly what that ladder exists to distrust — the worker writes pass N+1 a few
|
|
2373
|
+
* milliseconds AFTER flipping pass N to resolved, so a query landing in that gap
|
|
2374
|
+
* sees an empty queue for a chain that is very much alive. One look would turn
|
|
2375
|
+
* that into a confident "Indexed" with a green check, on the one scenario this
|
|
2376
|
+
* whole feature is for, and nothing would ever re-ask: the ladder is normally
|
|
2377
|
+
* triggered by a pass SETTLING, and after a reload there is no pass left to
|
|
2378
|
+
* settle. The ladder re-asks at 0/2s/6s, records each answer, and as a bonus
|
|
2379
|
+
* adopts and polls any live pass it finds, which makes the row genuinely active
|
|
2380
|
+
* instead of merely unconfirmed.
|
|
2381
|
+
*/
|
|
2382
|
+
refreshLiveIndexState() {
|
|
2383
|
+
this._adoptWorkerIndexingPasses(0);
|
|
2384
|
+
}
|
|
2385
|
+
/** Forget what we know about which files are indexing — but ONLY when the
|
|
2386
|
+
* snapshot was taken for a different chat than the one on screen now. For a
|
|
2387
|
+
* consumer whose history loading is its own fork and so never reaches
|
|
2388
|
+
* loadHistory's reset — a snapshot describes ONE chat's queue, and carrying it
|
|
2389
|
+
* into another project would let a row there claim to be finished on someone
|
|
2390
|
+
* else's evidence.
|
|
2391
|
+
*
|
|
2392
|
+
* Conditional for the same reason loadHistory's own reset is (the
|
|
2393
|
+
* `loadKey !== _liveIndexKey` gate): the view calls this on every mount, and
|
|
2394
|
+
* an unconditional wipe turned every re-entry to the chat into a grey
|
|
2395
|
+
* "Checking status:" sweep across rows whose state was already known. A
|
|
2396
|
+
* RE-entry keeps showing the last answer (green/yellow) while the first-page
|
|
2397
|
+
* refresh re-asks quietly; only a genuine project/platform switch starts from
|
|
2398
|
+
* "not known yet". Claiming `_liveIndexKey` here (before any answer) is the
|
|
2399
|
+
* same fudge loadHistory makes: it marks WHOSE chat the empty snapshot is
|
|
2400
|
+
* for, so repeated calls do not re-wipe, and _recordLiveIndexKeys re-claims
|
|
2401
|
+
* it when the real answer lands. */
|
|
2402
|
+
resetLiveIndexState() {
|
|
2403
|
+
var key = this.getHistoryCacheKey();
|
|
2404
|
+
if (key === this._liveIndexKey) return;
|
|
2405
|
+
this._liveIndexKey = key;
|
|
2406
|
+
this._resetLiveIndexKeys();
|
|
1863
2407
|
}
|
|
1864
2408
|
/** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
|
|
1865
2409
|
* live work from the moment it is sent, not from the moment it is acked. */
|
|
@@ -1877,6 +2421,31 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1877
2421
|
throw e;
|
|
1878
2422
|
});
|
|
1879
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Something just happened that plausibly ENDED indexing work, so let any waiting
|
|
2426
|
+
* turn look now instead of sitting out the rest of its busy interval.
|
|
2427
|
+
*
|
|
2428
|
+
* A nudge changes only WHEN a look happens, never what it concludes: the two
|
|
2429
|
+
* agreeing idle looks, the confirm gap between them, "a failed look counts as
|
|
2430
|
+
* busy" and the minimum wait are all untouched. That is why it is safe to fire
|
|
2431
|
+
* from places that are merely good guesses.
|
|
2432
|
+
*
|
|
2433
|
+
* Fired from end-of-chain points ONLY: the adopt ladder giving up, a resume
|
|
2434
|
+
* declining to continue, a pass failing. Not from every settling pass (one nudge
|
|
2435
|
+
* per pass per file for the whole run), and not from an indexing request being
|
|
2436
|
+
* accepted — see the note in trackIndexDispatch for why that one is actively
|
|
2437
|
+
* harmful rather than merely wasteful.
|
|
2438
|
+
*/
|
|
2439
|
+
_nudgeIndexingDrain() {
|
|
2440
|
+
if (!this._drainNudges.length) return;
|
|
2441
|
+
var list = this._drainNudges.slice();
|
|
2442
|
+
for (var i = 0; i < list.length; i++) {
|
|
2443
|
+
try {
|
|
2444
|
+
list[i]();
|
|
2445
|
+
} catch (e) {
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
1880
2449
|
/**
|
|
1881
2450
|
* Register a live poll so (a) a remount dedupes against it instead of stacking a
|
|
1882
2451
|
* SECOND poll on the same item, and (b) pausePolling can stop it.
|
|
@@ -1892,6 +2461,18 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1892
2461
|
this.historyItemPolls.set(id, { kind, stop });
|
|
1893
2462
|
return p;
|
|
1894
2463
|
}
|
|
2464
|
+
/** Background polls currently attached, for the MAX_CONCURRENT_BG_POLLS budget.
|
|
2465
|
+
* Counts the registry rather than a separate tally so it cannot drift: every
|
|
2466
|
+
* attach goes through _trackPoll and every detach deletes the entry. Note an
|
|
2467
|
+
* entry left behind by pausePolling on an older skapi-js (no stop handle)
|
|
2468
|
+
* still counts, which is correct — that poll really is still running. */
|
|
2469
|
+
_countBgPolls() {
|
|
2470
|
+
var n = 0;
|
|
2471
|
+
this.historyItemPolls.forEach(function(handle) {
|
|
2472
|
+
if (handle && handle.kind === "bg") n++;
|
|
2473
|
+
});
|
|
2474
|
+
return n;
|
|
2475
|
+
}
|
|
1895
2476
|
/**
|
|
1896
2477
|
* Stop and forget one item's poll. Used after a cancel: the row is either gone
|
|
1897
2478
|
* (cancelled while queued) or flagged cancelled (cancelled while running), so
|
|
@@ -1973,15 +2554,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1973
2554
|
}
|
|
1974
2555
|
getHistoryCacheKey() {
|
|
1975
2556
|
var id = this.host.getIdentity();
|
|
1976
|
-
if (!id.
|
|
1977
|
-
return id.
|
|
2557
|
+
if (!id.projectId || id.platform === "none") return "";
|
|
2558
|
+
return id.projectId + "#" + id.platform;
|
|
1978
2559
|
}
|
|
1979
2560
|
updateHistoryCache() {
|
|
1980
2561
|
var key = this.getHistoryCacheKey();
|
|
1981
2562
|
if (!key) return;
|
|
1982
2563
|
this.aiChatHistoryCache[key] = {
|
|
1983
2564
|
messages: this.state.messages.filter(function(m) {
|
|
1984
|
-
if (m._stageId) return false;
|
|
1985
2565
|
return m._ownerKey === void 0 || m._ownerKey === key;
|
|
1986
2566
|
}),
|
|
1987
2567
|
endOfList: this.state.historyEndOfList,
|
|
@@ -2028,6 +2608,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2028
2608
|
for (var j = 0; j < msgs.length; j++) {
|
|
2029
2609
|
var u = msgs[j];
|
|
2030
2610
|
if (!u || u.role !== "user" || u.isBackgroundTask) continue;
|
|
2611
|
+
if (u._stageId) continue;
|
|
2031
2612
|
if (!(u.isPendingQueued || u.isPendingInProcess || u.isSendingToServer)) continue;
|
|
2032
2613
|
if (serverId && u._serverItemId && u._serverItemId !== serverId) continue;
|
|
2033
2614
|
var settled = { role: "user", content: u.content };
|
|
@@ -2043,26 +2624,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2043
2624
|
};
|
|
2044
2625
|
}
|
|
2045
2626
|
/**
|
|
2046
|
-
*
|
|
2627
|
+
* projectId/owner are passed explicitly by every caller: a request can be
|
|
2047
2628
|
* dispatched after the user moved to another project, and re-reading the live
|
|
2048
2629
|
* identity here would silently send the turn to THAT project instead of the
|
|
2049
2630
|
* one it was composed for. Falls back to the live read only when a caller
|
|
2050
2631
|
* omits them.
|
|
2051
2632
|
*/
|
|
2052
|
-
_callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls,
|
|
2053
|
-
if (
|
|
2633
|
+
_callProviderFor(platform, prompt, messages, system, model, userId, extractContent, fileUrls, projectId, owner) {
|
|
2634
|
+
if (projectId === void 0 || owner === void 0) {
|
|
2054
2635
|
var id = this.host.getIdentity();
|
|
2055
|
-
if (
|
|
2636
|
+
if (projectId === void 0) projectId = id.projectId;
|
|
2056
2637
|
if (owner === void 0) owner = id.owner;
|
|
2057
2638
|
}
|
|
2058
|
-
return platform === "openai" ? callOpenAIWithPublicMcp(prompt,
|
|
2639
|
+
return platform === "openai" ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls) : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls);
|
|
2059
2640
|
}
|
|
2060
2641
|
dispatchAgentRequest(params) {
|
|
2061
2642
|
var self = this;
|
|
2062
2643
|
var dispatchItemId;
|
|
2063
2644
|
var sendAndPoll = function() {
|
|
2064
2645
|
return Promise.resolve(
|
|
2065
|
-
self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.
|
|
2646
|
+
self._callProviderFor(params.aiPlatform, params.text, params.boundedMessages, params.systemPrompt, params.aiModel, params.userId, params.extractContent, params.fileUrls, params.projectId, params.owner)
|
|
2066
2647
|
).then(function(initial) {
|
|
2067
2648
|
if (initial && initial.poll && (initial.status === "pending" || initial.status === "running")) {
|
|
2068
2649
|
if (initial.id) {
|
|
@@ -2146,16 +2727,60 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2146
2727
|
isPendingQueued: true,
|
|
2147
2728
|
isUploadingAttachments: true,
|
|
2148
2729
|
isSendingToServer: true,
|
|
2730
|
+
_dimSending: true,
|
|
2731
|
+
// A staged bubble has no server id for minutes, and its indexing rows are
|
|
2732
|
+
// now inserted ABOVE it — so its array index moves. Both views fall back to
|
|
2733
|
+
// the index when a bubble has no id, which would re-key (and in Vue, remount)
|
|
2734
|
+
// this bubble on every file, restarting its transition and losing it as a
|
|
2735
|
+
// scroll anchor. A local id it keeps for its whole life fixes both.
|
|
2736
|
+
_localId: this._newLocalId(),
|
|
2149
2737
|
_useBgQueue: true,
|
|
2150
2738
|
_stageId: stageId,
|
|
2151
2739
|
_ts: wallClockNow()
|
|
2152
2740
|
};
|
|
2153
2741
|
if (key) staged._ownerKey = key;
|
|
2742
|
+
this._liveStages[stageId] = true;
|
|
2154
2743
|
this.state.messages.push(staged);
|
|
2155
2744
|
this.host.notify();
|
|
2156
2745
|
this.host.scrollToBottom(true);
|
|
2157
2746
|
return stageId;
|
|
2158
2747
|
}
|
|
2748
|
+
/** Is anything in this page still uploading/dispatching for this stage? */
|
|
2749
|
+
isLiveStage(stageId) {
|
|
2750
|
+
return !!stageId && !!this._liveStages[stageId];
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Settle any staged bubble in `list` whose chain no longer exists, and return the
|
|
2754
|
+
* list (a new array only if something changed).
|
|
2755
|
+
*
|
|
2756
|
+
* The caller is a cache restore. A staged bubble is the one kind of message whose
|
|
2757
|
+
* resolution lives entirely in page memory — no server request stands behind it
|
|
2758
|
+
* yet — so a copy that outlives its upload would render "(Uploading files...)"
|
|
2759
|
+
* forever with nothing left to finish it. Today nothing can: this cache dies with
|
|
2760
|
+
* the page, so every restored stage is still live and this is a no-op. It exists
|
|
2761
|
+
* so that stops being a silent assumption.
|
|
2762
|
+
*/
|
|
2763
|
+
settleDeadStagedMessages(list) {
|
|
2764
|
+
if (!Array.isArray(list)) return list;
|
|
2765
|
+
var self = this;
|
|
2766
|
+
var dead = false;
|
|
2767
|
+
for (var i = 0; i < list.length; i++) {
|
|
2768
|
+
var m = list[i];
|
|
2769
|
+
if (m && m._stageId && !self._liveStages[m._stageId]) {
|
|
2770
|
+
dead = true;
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (!dead) return list;
|
|
2775
|
+
return list.map(function(m2) {
|
|
2776
|
+
if (!m2 || !m2._stageId || self._liveStages[m2._stageId]) return m2;
|
|
2777
|
+
var settled = { role: "user", content: m2.content };
|
|
2778
|
+
if (m2._ownerKey !== void 0) settled._ownerKey = m2._ownerKey;
|
|
2779
|
+
if (m2._ts !== void 0) settled._ts = m2._ts;
|
|
2780
|
+
if (m2._localId !== void 0) settled._localId = m2._localId;
|
|
2781
|
+
return settled;
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2159
2784
|
_stageIndex(list, stageId) {
|
|
2160
2785
|
if (!stageId) return -1;
|
|
2161
2786
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -2164,32 +2789,46 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2164
2789
|
return -1;
|
|
2165
2790
|
}
|
|
2166
2791
|
/**
|
|
2167
|
-
*
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2171
|
-
* are injected as each file's pass starts, after the bubble was staged.
|
|
2792
|
+
* Staged turn, phase 2: its files are up and it is now waiting for the whole
|
|
2793
|
+
* indexing chain behind them. Swaps "(Uploading files...)" for
|
|
2794
|
+
* "(Indexing files...)"; the bubble stays dimmed, because from the user's side
|
|
2795
|
+
* nothing has been handed over yet.
|
|
2172
2796
|
*
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2797
|
+
* It deliberately does NOT say "(In queue)" here. The turn is not queued behind
|
|
2798
|
+
* anything the server knows about yet — it is waiting on work that can run for
|
|
2799
|
+
* minutes — and claiming otherwise is what made the wait look like a stall.
|
|
2176
2800
|
*/
|
|
2177
|
-
|
|
2178
|
-
var lastBg = -1;
|
|
2179
|
-
for (var i = 0; i < this.state.messages.length; i++) {
|
|
2180
|
-
if (this.state.messages[i] && this.state.messages[i].isBackgroundTask) lastBg = i;
|
|
2181
|
-
}
|
|
2182
|
-
if (lastBg <= fromIdx) return -1;
|
|
2183
|
-
return lastBg;
|
|
2184
|
-
}
|
|
2185
|
-
/** The staged turn's files are up; it is now just waiting its place in the
|
|
2186
|
-
* queue. Drops the "(Uploading files...)" note back to "(In queue)". */
|
|
2187
|
-
markStagedMessageQueued(stageId) {
|
|
2801
|
+
markStagedMessageIndexing(stageId) {
|
|
2188
2802
|
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2189
2803
|
if (idx === -1) return;
|
|
2190
2804
|
var ex = this.state.messages[idx];
|
|
2191
2805
|
if (!ex.isUploadingAttachments) return;
|
|
2192
|
-
this.state.messages[idx] = Object.assign({}, ex, {
|
|
2806
|
+
this.state.messages[idx] = Object.assign({}, ex, {
|
|
2807
|
+
isUploadingAttachments: false,
|
|
2808
|
+
isAwaitingIndexing: true
|
|
2809
|
+
});
|
|
2810
|
+
this.host.notify();
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Staged turn, phase 3: the last of its files has finished indexing, so the turn
|
|
2814
|
+
* is genuinely just queued now. Full opacity + "(In queue)".
|
|
2815
|
+
*
|
|
2816
|
+
* Clears the PRESENTATIONAL _dimSending only; isSendingToServer stays set until
|
|
2817
|
+
* the server actually acks (it is the token that ack matches on). Called by the
|
|
2818
|
+
* clients the instant awaitIndexingDrained resolves, i.e. immediately before the
|
|
2819
|
+
* dispatch that replaces this bubble — dispatchComposedMessage carries the
|
|
2820
|
+
* cleared flag onto the replacement so the turn does not blink back to dimmed.
|
|
2821
|
+
*/
|
|
2822
|
+
markStagedMessageReady(stageId) {
|
|
2823
|
+
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2824
|
+
if (idx === -1) return;
|
|
2825
|
+
var ex = this.state.messages[idx];
|
|
2826
|
+
if (!ex.isAwaitingIndexing && !ex._dimSending && !ex.isUploadingAttachments) return;
|
|
2827
|
+
this.state.messages[idx] = Object.assign({}, ex, {
|
|
2828
|
+
isUploadingAttachments: false,
|
|
2829
|
+
isAwaitingIndexing: false,
|
|
2830
|
+
_dimSending: false
|
|
2831
|
+
});
|
|
2193
2832
|
this.host.notify();
|
|
2194
2833
|
}
|
|
2195
2834
|
/**
|
|
@@ -2216,7 +2855,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2216
2855
|
*/
|
|
2217
2856
|
awaitIndexingDrained(identity) {
|
|
2218
2857
|
var self = this;
|
|
2219
|
-
var svcId = identity && identity.
|
|
2858
|
+
var svcId = identity && identity.projectId;
|
|
2220
2859
|
var platform = identity && identity.platform;
|
|
2221
2860
|
if (!svcId || platform !== "claude" && platform !== "openai") return Promise.resolve("skipped");
|
|
2222
2861
|
var owner = identity.owner;
|
|
@@ -2225,11 +2864,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2225
2864
|
var deadline = startedAt + INDEXING_DRAIN_TIMEOUT_MS;
|
|
2226
2865
|
var idleLooks = 0;
|
|
2227
2866
|
var ask = function(status) {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2867
|
+
var answered = false;
|
|
2868
|
+
return new Promise(function(res) {
|
|
2869
|
+
var bail = null;
|
|
2870
|
+
var settle = function(v) {
|
|
2871
|
+
if (answered) return;
|
|
2872
|
+
answered = true;
|
|
2873
|
+
if (bail) {
|
|
2874
|
+
clearTimeout(bail);
|
|
2875
|
+
bail = null;
|
|
2876
|
+
}
|
|
2877
|
+
res(v);
|
|
2878
|
+
};
|
|
2879
|
+
bail = setTimeout(function() {
|
|
2880
|
+
settle(null);
|
|
2881
|
+
}, INDEXING_DRAIN_LOOK_TIMEOUT_MS);
|
|
2882
|
+
Promise.resolve(getChatHistory(
|
|
2883
|
+
{ service: svcId, owner, platform, queue, status },
|
|
2884
|
+
{ limit: WORKER_PASS_ADOPT_LIMIT }
|
|
2885
|
+
)).then(function(r) {
|
|
2886
|
+
settle(r);
|
|
2887
|
+
}, function() {
|
|
2888
|
+
settle(null);
|
|
2889
|
+
});
|
|
2233
2890
|
});
|
|
2234
2891
|
};
|
|
2235
2892
|
var hasLiveIndexing = function(res) {
|
|
@@ -2242,12 +2899,42 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2242
2899
|
return false;
|
|
2243
2900
|
};
|
|
2244
2901
|
return new Promise(function(resolve) {
|
|
2245
|
-
var
|
|
2246
|
-
|
|
2902
|
+
var timer = null;
|
|
2903
|
+
var lastLookAt = -Infinity;
|
|
2904
|
+
var nudgedThisInterval = false;
|
|
2905
|
+
var inFlight = false;
|
|
2906
|
+
var finish = function(v) {
|
|
2907
|
+
if (timer) {
|
|
2908
|
+
clearTimeout(timer);
|
|
2909
|
+
timer = null;
|
|
2910
|
+
}
|
|
2911
|
+
var ni = self._drainNudges.indexOf(nudge);
|
|
2912
|
+
if (ni !== -1) self._drainNudges.splice(ni, 1);
|
|
2913
|
+
resolve(v);
|
|
2914
|
+
};
|
|
2915
|
+
var again = function(ms) {
|
|
2916
|
+
if (timer) {
|
|
2917
|
+
clearTimeout(timer);
|
|
2918
|
+
timer = null;
|
|
2919
|
+
}
|
|
2920
|
+
var wait = ms == null ? idleLooks > 0 ? INDEXING_DRAIN_CONFIRM_POLL_MS : INDEXING_DRAIN_BUSY_POLL_MS : ms;
|
|
2921
|
+
timer = setTimeout(look, wait);
|
|
2922
|
+
};
|
|
2923
|
+
var nudge = function() {
|
|
2924
|
+
if (idleLooks > 0) return;
|
|
2925
|
+
if (inFlight) return;
|
|
2926
|
+
if (nudgedThisInterval) return;
|
|
2927
|
+
if (self._indexDispatchesInFlight > 0) return;
|
|
2928
|
+
nudgedThisInterval = true;
|
|
2929
|
+
again(Math.max(0, INDEXING_DRAIN_NUDGE_MIN_GAP_MS - (nowMs() - lastLookAt)));
|
|
2247
2930
|
};
|
|
2248
2931
|
var look = function() {
|
|
2932
|
+
timer = null;
|
|
2933
|
+
if (inFlight) return;
|
|
2934
|
+
lastLookAt = nowMs();
|
|
2935
|
+
nudgedThisInterval = false;
|
|
2249
2936
|
if (nowMs() >= deadline) {
|
|
2250
|
-
|
|
2937
|
+
finish("timedout");
|
|
2251
2938
|
return;
|
|
2252
2939
|
}
|
|
2253
2940
|
if (self._indexDispatchesInFlight > 0) {
|
|
@@ -2255,20 +2942,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2255
2942
|
again();
|
|
2256
2943
|
return;
|
|
2257
2944
|
}
|
|
2945
|
+
inFlight = true;
|
|
2258
2946
|
Promise.all([ask("running"), ask("pending")]).then(function(res) {
|
|
2947
|
+
inFlight = false;
|
|
2259
2948
|
var unknown = res[0] === null || res[1] === null;
|
|
2260
2949
|
if (unknown || hasLiveIndexing(res[0]) || hasLiveIndexing(res[1])) idleLooks = 0;
|
|
2261
2950
|
else idleLooks += 1;
|
|
2262
2951
|
if (idleLooks >= INDEXING_DRAIN_IDLE_LOOKS && nowMs() - startedAt >= INDEXING_DRAIN_MIN_MS) {
|
|
2263
|
-
|
|
2952
|
+
finish("drained");
|
|
2264
2953
|
return;
|
|
2265
2954
|
}
|
|
2266
2955
|
again();
|
|
2267
2956
|
}, function() {
|
|
2957
|
+
inFlight = false;
|
|
2268
2958
|
idleLooks = 0;
|
|
2269
2959
|
again();
|
|
2270
2960
|
});
|
|
2271
2961
|
};
|
|
2962
|
+
self._drainNudges.push(nudge);
|
|
2272
2963
|
look();
|
|
2273
2964
|
});
|
|
2274
2965
|
}
|
|
@@ -2279,12 +2970,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2279
2970
|
* failure separately.
|
|
2280
2971
|
*/
|
|
2281
2972
|
settleStagedMessage(stageId) {
|
|
2973
|
+
delete this._liveStages[stageId];
|
|
2282
2974
|
var idx = this._stageIndex(this.state.messages, stageId);
|
|
2283
2975
|
if (idx === -1) return;
|
|
2284
2976
|
var ex = this.state.messages[idx];
|
|
2285
2977
|
var settled = { role: "user", content: ex.content };
|
|
2286
2978
|
if (ex._ownerKey !== void 0) settled._ownerKey = ex._ownerKey;
|
|
2287
2979
|
if (ex._ts !== void 0) settled._ts = ex._ts;
|
|
2980
|
+
if (ex._localId !== void 0) settled._localId = ex._localId;
|
|
2288
2981
|
this.state.messages[idx] = settled;
|
|
2289
2982
|
this.host.notify();
|
|
2290
2983
|
this.updateHistoryCache();
|
|
@@ -2304,8 +2997,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2304
2997
|
if (stageId) this.settleStagedMessage(stageId);
|
|
2305
2998
|
return;
|
|
2306
2999
|
}
|
|
3000
|
+
if (stageId) delete this._liveStages[stageId];
|
|
2307
3001
|
var llmComposed = composedForLlm || composed;
|
|
2308
|
-
var key = !id.
|
|
3002
|
+
var key = !id.projectId ? "" : id.projectId + "#" + id.platform;
|
|
2309
3003
|
var offChat = !!key && key !== this.getHistoryCacheKey();
|
|
2310
3004
|
var isQueuedSend = !offChat && (useBgQueue || this.state.sending || this.state.messages.some(function(m) {
|
|
2311
3005
|
return (m.isPending || m.isPendingQueued) && !m.isBackgroundTask && !m._useBgQueue;
|
|
@@ -2313,7 +3007,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2313
3007
|
var aiPlatform = id.platform;
|
|
2314
3008
|
var aiModel = id.model || void 0;
|
|
2315
3009
|
var systemPrompt = pinned ? pinned.systemPrompt : this.host.buildSystemPrompt();
|
|
2316
|
-
var userId = id.userId || id.
|
|
3010
|
+
var userId = id.userId || id.projectId;
|
|
2317
3011
|
var chatQueue = useBgQueue ? bgIndexingQueueName(userId) : userId;
|
|
2318
3012
|
if (offChat) {
|
|
2319
3013
|
var offHistory = (this.aiChatHistoryCache[key] ? this.aiChatHistoryCache[key].messages : []).filter(function(m) {
|
|
@@ -2323,7 +3017,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2323
3017
|
platform: aiPlatform,
|
|
2324
3018
|
model: aiModel,
|
|
2325
3019
|
systemPrompt,
|
|
2326
|
-
|
|
3020
|
+
projectId: id.projectId,
|
|
2327
3021
|
history: offHistory.concat([{ role: "user", content: llmComposed }])
|
|
2328
3022
|
});
|
|
2329
3023
|
var offExisting = this.aiChatHistoryCache[key] || { messages: [], endOfList: false, startKeyHistory: [] };
|
|
@@ -2334,8 +3028,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2334
3028
|
this.state.messages.splice(offStage, 1);
|
|
2335
3029
|
this.host.notify();
|
|
2336
3030
|
}
|
|
3031
|
+
var offCached = offExisting.messages;
|
|
3032
|
+
if (stageId) {
|
|
3033
|
+
offCached = offCached.filter(function(m) {
|
|
3034
|
+
if (m._stageId !== stageId) return true;
|
|
3035
|
+
if (offStage === -1 && m._ts !== void 0) offUser._ts = m._ts;
|
|
3036
|
+
return false;
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
2337
3039
|
this.aiChatHistoryCache[key] = {
|
|
2338
|
-
messages:
|
|
3040
|
+
messages: offCached.concat([
|
|
2339
3041
|
offUser,
|
|
2340
3042
|
{ role: "assistant", content: "", isPending: true, isPendingInProcess: true, _ownerKey: key }
|
|
2341
3043
|
]),
|
|
@@ -2344,7 +3046,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2344
3046
|
};
|
|
2345
3047
|
this.dispatchAgentRequest({
|
|
2346
3048
|
key,
|
|
2347
|
-
|
|
3049
|
+
projectId: id.projectId,
|
|
2348
3050
|
owner: id.owner,
|
|
2349
3051
|
aiPlatform,
|
|
2350
3052
|
aiModel,
|
|
@@ -2365,22 +3067,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2365
3067
|
platform: aiPlatform,
|
|
2366
3068
|
model: aiModel,
|
|
2367
3069
|
systemPrompt,
|
|
2368
|
-
|
|
3070
|
+
projectId: id.projectId,
|
|
2369
3071
|
history: resolvedHistory.concat([{ role: "user", content: llmComposed }])
|
|
2370
3072
|
});
|
|
2371
|
-
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _ts: wallClockNow() };
|
|
3073
|
+
var queuedBubble = { role: "user", content: composed, isPendingQueued: true, isSendingToServer: true, _dimSending: true, _localId: this._newLocalId(), _ts: wallClockNow() };
|
|
2372
3074
|
if (key) queuedBubble._ownerKey = key;
|
|
2373
3075
|
if (useBgQueue) queuedBubble._useBgQueue = true;
|
|
2374
3076
|
var qStage = this._stageIndex(this.state.messages, stageId);
|
|
2375
3077
|
if (qStage !== -1) {
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
if (
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
this.state.messages.splice(qStage, 1);
|
|
2382
|
-
this.state.messages.splice(qTarget, 0, queuedBubble);
|
|
2383
|
-
}
|
|
3078
|
+
var qEx = this.state.messages[qStage];
|
|
3079
|
+
if (qEx._ts !== void 0) queuedBubble._ts = qEx._ts;
|
|
3080
|
+
if (qEx._dimSending === false) queuedBubble._dimSending = false;
|
|
3081
|
+
if (qEx._localId) queuedBubble._localId = qEx._localId;
|
|
3082
|
+
this.state.messages.splice(qStage, 1, queuedBubble);
|
|
2384
3083
|
} else {
|
|
2385
3084
|
this.state.messages.push(queuedBubble);
|
|
2386
3085
|
}
|
|
@@ -2388,13 +3087,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2388
3087
|
this.updateHistoryCache();
|
|
2389
3088
|
this.host.scrollToBottom(true);
|
|
2390
3089
|
var capturedComposed = composed, capturedPlatform = aiPlatform, capturedKey = key;
|
|
2391
|
-
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.
|
|
3090
|
+
Promise.resolve(this._callProviderFor(aiPlatform, composed, boundedQ.messages, systemPrompt, aiModel, chatQueue, extractContent, fileUrls, id.projectId, id.owner)).then(function(result) {
|
|
2392
3091
|
var sendingIdx = self.getHistoryCacheKey() !== capturedKey ? -1 : self.state.messages.findIndex(function(m) {
|
|
2393
3092
|
return m.isSendingToServer && (m.isPendingQueued || m.isPendingInProcess) && m.role === "user" && !m._stageId && (m._ownerKey === void 0 || m._ownerKey === capturedKey);
|
|
2394
3093
|
});
|
|
2395
3094
|
var serverId = result && typeof result.id === "string" ? result.id : void 0;
|
|
2396
3095
|
if (sendingIdx >= 0) {
|
|
2397
|
-
var upd = Object.assign({}, self.state.messages[sendingIdx], { isSendingToServer: false });
|
|
3096
|
+
var upd = Object.assign({}, self.state.messages[sendingIdx], { isSendingToServer: false, _dimSending: false });
|
|
2398
3097
|
if (serverId) upd._serverItemId = serverId;
|
|
2399
3098
|
self.state.messages[sendingIdx] = upd;
|
|
2400
3099
|
self.host.notify();
|
|
@@ -2415,18 +3114,14 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2415
3114
|
});
|
|
2416
3115
|
return;
|
|
2417
3116
|
}
|
|
2418
|
-
var immediateUser = { role: "user", content: composed, _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
3117
|
+
var immediateUser = { role: "user", content: composed, _localId: this._newLocalId(), _ts: wallClockNow(), ...key ? { _ownerKey: key } : {} };
|
|
2419
3118
|
var immediatePlaceholder = { role: "assistant", content: "", isPending: true, isPendingInProcess: true, ...key ? { _ownerKey: key } : {} };
|
|
2420
3119
|
var iStage = this._stageIndex(this.state.messages, stageId);
|
|
2421
3120
|
if (iStage !== -1) {
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
if (
|
|
2425
|
-
|
|
2426
|
-
} else {
|
|
2427
|
-
this.state.messages.splice(iStage, 1);
|
|
2428
|
-
this.state.messages.splice(iTarget, 0, immediateUser, immediatePlaceholder);
|
|
2429
|
-
}
|
|
3121
|
+
var iEx = this.state.messages[iStage];
|
|
3122
|
+
if (iEx._ts !== void 0) immediateUser._ts = iEx._ts;
|
|
3123
|
+
if (iEx._localId) immediateUser._localId = iEx._localId;
|
|
3124
|
+
this.state.messages.splice(iStage, 1, immediateUser, immediatePlaceholder);
|
|
2430
3125
|
} else {
|
|
2431
3126
|
this.state.messages.push(immediateUser);
|
|
2432
3127
|
this.state.messages.push(immediatePlaceholder);
|
|
@@ -2444,12 +3139,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2444
3139
|
platform: aiPlatform,
|
|
2445
3140
|
model: aiModel,
|
|
2446
3141
|
systemPrompt,
|
|
2447
|
-
|
|
3142
|
+
projectId: id.projectId,
|
|
2448
3143
|
history: historyForLlm
|
|
2449
3144
|
});
|
|
2450
3145
|
var run = this.dispatchAgentRequest({
|
|
2451
3146
|
key,
|
|
2452
|
-
|
|
3147
|
+
projectId: id.projectId,
|
|
2453
3148
|
owner: id.owner,
|
|
2454
3149
|
aiPlatform,
|
|
2455
3150
|
aiModel,
|
|
@@ -2506,6 +3201,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2506
3201
|
if (existing._serverItemId !== void 0) promoted._serverItemId = existing._serverItemId;
|
|
2507
3202
|
if (existing._ownerKey !== void 0) promoted._ownerKey = existing._ownerKey;
|
|
2508
3203
|
if (existing.isSendingToServer) promoted.isSendingToServer = true;
|
|
3204
|
+
if (existing._dimSending) promoted._dimSending = true;
|
|
3205
|
+
if (existing._localId !== void 0) promoted._localId = existing._localId;
|
|
2509
3206
|
this.state.messages[nextIdx] = promoted;
|
|
2510
3207
|
var placeholder = { role: "assistant", content: "", isPending: true };
|
|
2511
3208
|
if (existing._serverItemId !== void 0) placeholder._serverItemId = existing._serverItemId;
|
|
@@ -2513,6 +3210,33 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2513
3210
|
this.state.messages.splice(nextIdx + 1, 0, placeholder);
|
|
2514
3211
|
this.host.notify();
|
|
2515
3212
|
}
|
|
3213
|
+
/**
|
|
3214
|
+
* The "Thinking..." placeholder belonging to the user bubble at `userIdx`, or -1.
|
|
3215
|
+
*
|
|
3216
|
+
* Every path that creates one puts it IMMEDIATELY after its user bubble
|
|
3217
|
+
* (promoteNextQueuedToRunning, the immediate-send pair, applyHistoryItemResolution),
|
|
3218
|
+
* so ownership is adjacency — modulo background bubbles, which get spliced in
|
|
3219
|
+
* around them. Taking the first pending assistant ANYWHERE below instead was a
|
|
3220
|
+
* hijack: a turn sent with attachments never gets a placeholder of its own
|
|
3221
|
+
* (promoteNextQueuedToRunning skips _useBgQueue turns) and now keeps the position
|
|
3222
|
+
* it was sent in, so an ordinary turn sent while its files indexed sits BELOW it
|
|
3223
|
+
* with a placeholder of its own — and the attachment turn's answer was rendered
|
|
3224
|
+
* as the answer to that unrelated question.
|
|
3225
|
+
*/
|
|
3226
|
+
_ownThinkingIndex(userIdx, serverId) {
|
|
3227
|
+
if (userIdx < 0) return -1;
|
|
3228
|
+
for (var i = userIdx + 1; i < this.state.messages.length; i++) {
|
|
3229
|
+
var m = this.state.messages[i];
|
|
3230
|
+
if (!m) return -1;
|
|
3231
|
+
if (m.isBackgroundTask) continue;
|
|
3232
|
+
if (m.isPending && m.role === "assistant") {
|
|
3233
|
+
if (serverId && m._serverItemId && m._serverItemId !== serverId) return -1;
|
|
3234
|
+
return i;
|
|
3235
|
+
}
|
|
3236
|
+
return -1;
|
|
3237
|
+
}
|
|
3238
|
+
return -1;
|
|
3239
|
+
}
|
|
2516
3240
|
resolveQueuedUserBubble(serverId) {
|
|
2517
3241
|
var liveKey = this.getHistoryCacheKey();
|
|
2518
3242
|
var isLocal = function(m) {
|
|
@@ -2539,9 +3263,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2539
3263
|
if (userIdx >= 0) {
|
|
2540
3264
|
var ex = this.state.messages[userIdx];
|
|
2541
3265
|
this.state.messages[userIdx] = { role: "user", content: ex.content, isCancelled: true, _serverItemId: ex._serverItemId, ...ex._ownerKey !== void 0 ? { _ownerKey: ex._ownerKey } : {} };
|
|
2542
|
-
var thIdx = this.
|
|
2543
|
-
return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
|
|
2544
|
-
});
|
|
3266
|
+
var thIdx = this._ownThinkingIndex(userIdx, serverId);
|
|
2545
3267
|
if (thIdx !== -1) this.state.messages.splice(thIdx, 1);
|
|
2546
3268
|
}
|
|
2547
3269
|
this.promoteNextQueuedToRunning();
|
|
@@ -2553,16 +3275,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2553
3275
|
if (exist._serverItemId !== void 0) repl._serverItemId = exist._serverItemId;
|
|
2554
3276
|
if (exist._ownerKey !== void 0) repl._ownerKey = exist._ownerKey;
|
|
2555
3277
|
if (exist._ts !== void 0) repl._ts = exist._ts;
|
|
3278
|
+
if (exist._localId !== void 0) repl._localId = exist._localId;
|
|
2556
3279
|
this.state.messages[userIdx] = repl;
|
|
2557
3280
|
}
|
|
2558
|
-
var thinkingIdx =
|
|
2559
|
-
return i > userIdx && m.isPending && m.role === "assistant" && !m.isBackgroundTask;
|
|
2560
|
-
}) : -1;
|
|
3281
|
+
var thinkingIdx = this._ownThinkingIndex(userIdx, serverId);
|
|
2561
3282
|
return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
|
|
2562
3283
|
}
|
|
2563
3284
|
insertAtTarget(msg, targetIdx) {
|
|
2564
3285
|
if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = wallClockNow();
|
|
2565
|
-
|
|
3286
|
+
var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
|
|
3287
|
+
var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
|
|
3288
|
+
if (replaceable) this.state.messages[targetIdx] = msg;
|
|
2566
3289
|
else if (targetIdx >= 0) this.state.messages.splice(targetIdx, 0, msg);
|
|
2567
3290
|
else this.state.messages.push(msg);
|
|
2568
3291
|
}
|
|
@@ -2679,16 +3402,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2679
3402
|
var platform = id.platform;
|
|
2680
3403
|
if (platform !== "claude" && platform !== "openai") return;
|
|
2681
3404
|
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
2682
|
-
var queueBase = id.userId || id.
|
|
3405
|
+
var queueBase = id.userId || id.projectId;
|
|
2683
3406
|
var queue = msg.isBackgroundTask || msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
2684
|
-
this.state.messages[idx]
|
|
3407
|
+
var at = this.state.messages[idx] && this.state.messages[idx]._serverItemId === serverId && this.state.messages[idx].role === msg.role ? idx : this.state.messages.findIndex(function(m) {
|
|
3408
|
+
return m._serverItemId === serverId && m.role === msg.role;
|
|
3409
|
+
});
|
|
3410
|
+
if (at !== -1) {
|
|
3411
|
+
this.state.messages[at] = Object.assign({}, this.state.messages[at], { _cancelling: true, _cancelError: void 0 });
|
|
3412
|
+
}
|
|
2685
3413
|
this.host.notify();
|
|
2686
3414
|
Promise.resolve(this.host.cancelRequest({
|
|
2687
3415
|
url,
|
|
2688
3416
|
method: "POST",
|
|
2689
3417
|
id: serverId,
|
|
2690
3418
|
queue,
|
|
2691
|
-
service: id.
|
|
3419
|
+
service: id.projectId,
|
|
2692
3420
|
owner: id.owner
|
|
2693
3421
|
})).then(function(result) {
|
|
2694
3422
|
if (result && result.removed) {
|
|
@@ -2758,7 +3486,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2758
3486
|
* 2. the file is remembered in cancelledIndexKeys, so the client-driven
|
|
2759
3487
|
* resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
|
|
2760
3488
|
* 3. any of its passes still sitting in bgTaskQueue is dropped by the next
|
|
2761
|
-
* drain rather than surfacing a fresh "Indexing…" bubble
|
|
3489
|
+
* drain rather than surfacing a fresh "Indexing…" bubble; and
|
|
3490
|
+
* 4. the RUN is remembered (state.stoppedIndexIds), because none of the above
|
|
3491
|
+
* necessarily leaves a mark on the conversation — see below — and without
|
|
3492
|
+
* it the collapsed row reported the stopped file as finished.
|
|
2762
3493
|
*
|
|
2763
3494
|
* Records already written by the passes that DID run are kept — this stops the
|
|
2764
3495
|
* work, it does not undo it.
|
|
@@ -2768,6 +3499,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2768
3499
|
if (!group || !group.key) return;
|
|
2769
3500
|
var scoped = this.getHistoryCacheKey() + "|" + group.key;
|
|
2770
3501
|
this.cancelledIndexKeys.add(scoped);
|
|
3502
|
+
if (!group.finished) {
|
|
3503
|
+
var stoppedIds = {};
|
|
3504
|
+
for (var sk in this.state.stoppedIndexIds) stoppedIds[sk] = true;
|
|
3505
|
+
(group.members || []).forEach(function(m) {
|
|
3506
|
+
var sid = m && m.msg && m.msg._serverItemId;
|
|
3507
|
+
if (sid) stoppedIds[sid] = true;
|
|
3508
|
+
});
|
|
3509
|
+
this.bgTaskQueue.forEach(function(e) {
|
|
3510
|
+
if (e && e.id && self._indexKeyOf(e) === scoped) stoppedIds[e.id] = true;
|
|
3511
|
+
});
|
|
3512
|
+
this.state.stoppedIndexIds = stoppedIds;
|
|
3513
|
+
}
|
|
3514
|
+
this._adoptWorkerIndexingPasses(0);
|
|
2771
3515
|
var ids = group.cancellableIds || [];
|
|
2772
3516
|
if (!ids.length) {
|
|
2773
3517
|
this.host.notify();
|
|
@@ -2941,21 +3685,69 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2941
3685
|
this.promoteNextQueuedToRunning();
|
|
2942
3686
|
return this.enqueueTypewrite(pendingIdx, latest.content, lid);
|
|
2943
3687
|
}
|
|
2944
|
-
// Remove
|
|
2945
|
-
//
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
//
|
|
2949
|
-
//
|
|
2950
|
-
//
|
|
2951
|
-
//
|
|
2952
|
-
//
|
|
2953
|
-
//
|
|
3688
|
+
// Remove leftover non-background pending ("Thinking…") assistant bubbles: the
|
|
3689
|
+
// duplicate that appears when a concurrent history refetch re-maps the still-
|
|
3690
|
+
// "running" turn into a pending placeholder (with a real _serverItemId) while the
|
|
3691
|
+
// local pending bubble (no _serverItemId) is rescued and re-appended (see the
|
|
3692
|
+
// loadHistory rescue below), and the orphan a resolve leaves when it splices its
|
|
3693
|
+
// reply beside a placeholder instead of into it. Each resolve path only replaces
|
|
3694
|
+
// ONE pending bubble, so without this a stray "Thinking…" survives forever next to
|
|
3695
|
+
// the reply. MUST run AFTER the resolved bubble has been made non-pending and
|
|
3696
|
+
// BEFORE promoteNext*() (which only adds a Thinking once none remains).
|
|
3697
|
+
//
|
|
3698
|
+
// It used to take EVERY one, on the premise that there is at most one at a time
|
|
3699
|
+
// because promoteNext* refuses to add a second. That premise never covered the
|
|
3700
|
+
// immediate-send path, which creates its pair directly — and a turn sent with
|
|
3701
|
+
// attachments does not block the composer and resolves on its own queue, so an
|
|
3702
|
+
// ordinary question asked while files index is in flight, with a placeholder of
|
|
3703
|
+
// its own, exactly when the attachment turn resolves. Sweeping it left that
|
|
3704
|
+
// question with no spinner and, worse, nowhere for its answer to land:
|
|
3705
|
+
// typewriteLatestReply bails when there is no pending assistant, so the reply
|
|
3706
|
+
// reached the cache and never the screen.
|
|
3707
|
+
//
|
|
3708
|
+
// The discriminator is the owning USER bubble. A live immediate send's user bubble
|
|
3709
|
+
// carries NO pending flags (its in-flight-ness lives in state.sending), while every
|
|
3710
|
+
// duplicate this sweep is for belongs to a user bubble that is still pending — and
|
|
3711
|
+
// an orphan has no user bubble above it at all.
|
|
2954
3712
|
_removeStrayPendingAssistants() {
|
|
2955
3713
|
for (var k = this.state.messages.length - 1; k >= 0; k--) {
|
|
2956
3714
|
var m = this.state.messages[k];
|
|
2957
|
-
if (m.isPending
|
|
3715
|
+
if (!m || !m.isPending || m.role !== "assistant" || m.isBackgroundTask) continue;
|
|
3716
|
+
if (this._isLiveImmediatePlaceholder(k)) continue;
|
|
3717
|
+
this.state.messages.splice(k, 1);
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
/** Index of the USER bubble the message at `idx` belongs to — the nearest one
|
|
3721
|
+
* above it, stepping over background bubbles (a file's indexing rows are
|
|
3722
|
+
* inserted between turns). -1 when the nearest thing above is not a user turn,
|
|
3723
|
+
* which for a placeholder means it is an orphan. */
|
|
3724
|
+
_owningUserIndex(idx) {
|
|
3725
|
+
for (var j = idx - 1; j >= 0; j--) {
|
|
3726
|
+
var p = this.state.messages[j];
|
|
3727
|
+
if (!p) return -1;
|
|
3728
|
+
if (p.isBackgroundTask) continue;
|
|
3729
|
+
return p.role === "user" ? j : -1;
|
|
2958
3730
|
}
|
|
3731
|
+
return -1;
|
|
3732
|
+
}
|
|
3733
|
+
/** The bubble at `idx` is the "Thinking…" of a DIFFERENT turn that is still
|
|
3734
|
+
* waiting for its answer, so the sweep above must leave it alone. */
|
|
3735
|
+
_isLiveImmediatePlaceholder(idx) {
|
|
3736
|
+
var ui = this._owningUserIndex(idx);
|
|
3737
|
+
if (ui === -1) return false;
|
|
3738
|
+
var p = this.state.messages[ui];
|
|
3739
|
+
return !p.isPending && !p.isPendingQueued && !p.isPendingInProcess && !p.isPendingOlder && !p.isSendingToServer && !p.isCancelled;
|
|
3740
|
+
}
|
|
3741
|
+
/** A pending assistant at `idx` is the placeholder OF the turn above it, so a
|
|
3742
|
+
* reply may take its slot. Every path that makes one copies the parent's
|
|
3743
|
+
* _serverItemId (or neither has one yet), so a mismatch means the slot belongs to
|
|
3744
|
+
* some other request and the reply must be spliced in beside it, not on top. */
|
|
3745
|
+
_isOwnPlaceholderOf(idx, userIdx) {
|
|
3746
|
+
if (userIdx === -1) return false;
|
|
3747
|
+
var ph = this.state.messages[idx], u = this.state.messages[userIdx];
|
|
3748
|
+
if (!ph || !u) return false;
|
|
3749
|
+
if (ph._serverItemId === void 0 || u._serverItemId === void 0) return true;
|
|
3750
|
+
return ph._serverItemId === u._serverItemId;
|
|
2959
3751
|
}
|
|
2960
3752
|
// Drop the pending flags on the resolved turn's USER bubble (preserving its
|
|
2961
3753
|
// content + background-task marker). Needed because a bg "Indexing:" turn's user
|
|
@@ -3003,6 +3795,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3003
3795
|
var indexRef = this._indexRefOfItem(itemId);
|
|
3004
3796
|
this.applyHistoryItemResolution(itemId, response, platform);
|
|
3005
3797
|
this.promoteNextBgQueuedToRunning();
|
|
3798
|
+
this.drainBgTaskQueue();
|
|
3006
3799
|
if (indexRef) this._followWorkerIndexingChain(indexRef.name, indexRef.mime);
|
|
3007
3800
|
}
|
|
3008
3801
|
/** The file an already-rendered background pass is about, off its request
|
|
@@ -3016,11 +3809,67 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3016
3809
|
}
|
|
3017
3810
|
return null;
|
|
3018
3811
|
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Settle a turn the server reports as cancelled: the request bubble goes to its
|
|
3814
|
+
* cancelled form and the "Thinking..." placeholder goes away. The same shape
|
|
3815
|
+
* cancelQueuedMessage produces locally, so a cancel this client made and one it
|
|
3816
|
+
* merely found out about render identically — and an indexing pass keeps the
|
|
3817
|
+
* markers that hold it in its file's collapsed row.
|
|
3818
|
+
*/
|
|
3819
|
+
_settleCancelledItem(itemId) {
|
|
3820
|
+
var uIdx = this.state.messages.findIndex(function(m) {
|
|
3821
|
+
return m.role === "user" && m._serverItemId === itemId && !m.isCancelled;
|
|
3822
|
+
});
|
|
3823
|
+
if (uIdx !== -1) {
|
|
3824
|
+
var u = this.state.messages[uIdx];
|
|
3825
|
+
var cancelled = { role: "user", content: u.content, isCancelled: true, _serverItemId: itemId };
|
|
3826
|
+
if (u.isBackgroundTask) cancelled.isBackgroundTask = true;
|
|
3827
|
+
if (u._indexFile) cancelled._indexFile = u._indexFile;
|
|
3828
|
+
if (u._useBgQueue) cancelled._useBgQueue = true;
|
|
3829
|
+
if (u._ownerKey !== void 0) cancelled._ownerKey = u._ownerKey;
|
|
3830
|
+
if (u._ts !== void 0) cancelled._ts = u._ts;
|
|
3831
|
+
this.state.messages[uIdx] = cancelled;
|
|
3832
|
+
}
|
|
3833
|
+
var pIdx = this.state.messages.findIndex(function(m) {
|
|
3834
|
+
return m.isPending && m.role === "assistant" && m._serverItemId === itemId;
|
|
3835
|
+
});
|
|
3836
|
+
if (pIdx !== -1) this.state.messages.splice(pIdx, 1);
|
|
3837
|
+
this.cancelledServerIds.delete(itemId);
|
|
3838
|
+
this._removeStrayPendingAssistants();
|
|
3839
|
+
this.host.notify();
|
|
3840
|
+
this.updateHistoryCache();
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* A poll that came back saying the request was CANCELLED, rather than with an
|
|
3844
|
+
* answer.
|
|
3845
|
+
*
|
|
3846
|
+
* The server keeps a cancelled request as a terminal row instead of deleting it
|
|
3847
|
+
* (that row is the durable record of the stop, and the chat history it belongs
|
|
3848
|
+
* to), so a poll still running when the cancel lands now RESOLVES on it. It used
|
|
3849
|
+
* to reject with NOT_EXISTS, and the resolution path below reads a status object
|
|
3850
|
+
* as an answer with no text — which would stamp "No text response received from
|
|
3851
|
+
* AI provider" over a turn the user had just stopped.
|
|
3852
|
+
*
|
|
3853
|
+
* Reachable whenever the poll was not stopped by whoever cancelled: another tab,
|
|
3854
|
+
* another device, or the row being cancelled server-side by the file's own stop.
|
|
3855
|
+
*/
|
|
3856
|
+
_isCancelledPollResult(response) {
|
|
3857
|
+
if (!response || typeof response !== "object" || response.status !== "cancelled") return false;
|
|
3858
|
+
if (response.content !== void 0 || response.output !== void 0) return false;
|
|
3859
|
+
return response.queue_name !== void 0 || response.in_queue !== void 0;
|
|
3860
|
+
}
|
|
3019
3861
|
applyHistoryItemResolution(itemId, response, platform) {
|
|
3020
3862
|
this.historyItemPolls.delete(itemId);
|
|
3863
|
+
if (this._isCancelledPollResult(response)) {
|
|
3864
|
+
this._settleCancelledItem(itemId);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3021
3867
|
var isErr = isErrorResponseBody(response);
|
|
3022
3868
|
var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
|
|
3023
|
-
|
|
3869
|
+
var reportedComplete = !isErr && !!answer && answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
|
|
3870
|
+
var stripMarker = function(t) {
|
|
3871
|
+
return reportedComplete ? t.split(INDEXING_COMPLETE_MARKER).join("").trim() : t;
|
|
3872
|
+
};
|
|
3024
3873
|
var idx = this.state.messages.findIndex(function(m) {
|
|
3025
3874
|
return m.isPending && m._serverItemId === itemId;
|
|
3026
3875
|
});
|
|
@@ -3036,7 +3885,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3036
3885
|
}
|
|
3037
3886
|
var text = answer || "No text response received from AI provider.";
|
|
3038
3887
|
if (wasBgTask) {
|
|
3039
|
-
this.state.messages[idx] = { role: "assistant", content: text, isBackgroundTask: true, _serverItemId: itemId };
|
|
3888
|
+
this.state.messages[idx] = { role: "assistant", content: stripMarker(text) || EMPTY_INDEXING_REPLY, isBackgroundTask: true, _serverItemId: itemId, ...reportedComplete ? { _indexComplete: true } : {} };
|
|
3040
3889
|
this.host.notify();
|
|
3041
3890
|
this.updateHistoryCache();
|
|
3042
3891
|
return;
|
|
@@ -3069,7 +3918,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3069
3918
|
}
|
|
3070
3919
|
var text2 = answer || "No text response received from AI provider.";
|
|
3071
3920
|
if (ex.isBackgroundTask) {
|
|
3072
|
-
this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: text2, isBackgroundTask: true, _serverItemId: itemId });
|
|
3921
|
+
this.state.messages.splice(userIdx + 1, 0, { role: "assistant", content: stripMarker(text2) || EMPTY_INDEXING_REPLY, isBackgroundTask: true, _serverItemId: itemId, ...reportedComplete ? { _indexComplete: true } : {} });
|
|
3073
3922
|
this.host.notify();
|
|
3074
3923
|
this.updateHistoryCache();
|
|
3075
3924
|
return;
|
|
@@ -3090,7 +3939,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3090
3939
|
if (!entry) return "";
|
|
3091
3940
|
var file = entry.storagePath || entry.filename;
|
|
3092
3941
|
if (!file) return "";
|
|
3093
|
-
return entry.
|
|
3942
|
+
return entry.projectId + "#" + entry.platform + "|" + file;
|
|
3094
3943
|
}
|
|
3095
3944
|
/**
|
|
3096
3945
|
* Reconcile the bg queue with the files the user has stopped.
|
|
@@ -3100,17 +3949,33 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3100
3949
|
* path, and without this an earlier cancel would silently kill every future
|
|
3101
3950
|
* index of the same path. A continuation of a stopped file is dropped instead,
|
|
3102
3951
|
* covering the pass that was dispatched in the moment before the cancel landed.
|
|
3952
|
+
*
|
|
3953
|
+
* "Fresh" is the load-bearing word, and it used to be missing. A run's OWN first
|
|
3954
|
+
* pass sits in this queue for as long as it runs (entries are only dropped once
|
|
3955
|
+
* their bubble settles), so stopping a file during its first pass — which is
|
|
3956
|
+
* exactly when a user who has just uploaded it does — met that first-pass entry
|
|
3957
|
+
* on the very next drain and lifted the stop the user had just asked for. The
|
|
3958
|
+
* chain then carried on, one worker-minted window after another, with nothing
|
|
3959
|
+
* client-side left to suppress it. The ids recorded at stop time are what tells
|
|
3960
|
+
* the two apart: a pass that was already there when the user hit Stop cannot be
|
|
3961
|
+
* the new request that lifts it.
|
|
3103
3962
|
*/
|
|
3104
3963
|
_applyIndexCancellations() {
|
|
3105
3964
|
if (!this.cancelledIndexKeys.size) return;
|
|
3965
|
+
var surfaced = {};
|
|
3966
|
+
this.state.messages.forEach(function(m) {
|
|
3967
|
+
if (!m._serverItemId) return;
|
|
3968
|
+
if (m.isPending || m.isPendingQueued || m.isPendingInProcess) surfaced[m._serverItemId] = true;
|
|
3969
|
+
});
|
|
3106
3970
|
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
3107
3971
|
var entry = this.bgTaskQueue[i];
|
|
3108
3972
|
var key = this._indexKeyOf(entry);
|
|
3109
3973
|
if (!key || !this.cancelledIndexKeys.has(key)) continue;
|
|
3110
|
-
if (!entry.resumePass) {
|
|
3974
|
+
if (!entry.resumePass && !this.state.stoppedIndexIds[entry.id]) {
|
|
3111
3975
|
this.cancelledIndexKeys.delete(key);
|
|
3112
3976
|
continue;
|
|
3113
3977
|
}
|
|
3978
|
+
if (surfaced[entry.id]) continue;
|
|
3114
3979
|
this.bgTaskQueue.splice(i, 1);
|
|
3115
3980
|
this._stopPoll(entry.id);
|
|
3116
3981
|
this._cancelServerItem(entry.id);
|
|
@@ -3163,10 +4028,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3163
4028
|
if (this._adoptingWorkerPasses) return;
|
|
3164
4029
|
var id = this.host.getIdentity();
|
|
3165
4030
|
var platform = id.platform;
|
|
3166
|
-
if (!id.
|
|
4031
|
+
if (!id.projectId || platform !== "claude" && platform !== "openai") return;
|
|
3167
4032
|
if (this.isPollingPaused() || !this.host.isViewMounted()) return;
|
|
3168
|
-
var svcId = id.
|
|
3169
|
-
var queue = bgIndexingQueueName(id.userId, id.
|
|
4033
|
+
var svcId = id.projectId, owner = id.owner;
|
|
4034
|
+
var queue = bgIndexingQueueName(id.userId, id.projectId);
|
|
3170
4035
|
var ask = function(status) {
|
|
3171
4036
|
return Promise.resolve(getChatHistory(
|
|
3172
4037
|
{ service: svcId, owner, platform, queue, status },
|
|
@@ -3179,8 +4044,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3179
4044
|
Promise.all([ask("running"), ask("pending")]).then(function(results) {
|
|
3180
4045
|
self._adoptingWorkerPasses = false;
|
|
3181
4046
|
var now = self.host.getIdentity();
|
|
3182
|
-
if (now.
|
|
4047
|
+
if (now.projectId !== svcId || now.platform !== platform) return;
|
|
3183
4048
|
if (!self.host.isViewMounted()) return;
|
|
4049
|
+
if (results[0] !== null && results[1] !== null) self._recordLiveIndexKeys(results);
|
|
3184
4050
|
var adoptedIds = [];
|
|
3185
4051
|
for (var ri = 0; ri < results.length; ri++) {
|
|
3186
4052
|
var list = results[ri] && Array.isArray(results[ri].list) ? results[ri].list : [];
|
|
@@ -3192,10 +4058,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3192
4058
|
self.drainBgTaskQueue();
|
|
3193
4059
|
if (self._isTrackingAny(adoptedIds)) return;
|
|
3194
4060
|
}
|
|
3195
|
-
if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length)
|
|
4061
|
+
if (attempt + 1 >= WORKER_PASS_ADOPT_ATTEMPTS.length) {
|
|
4062
|
+
self._nudgeIndexingDrain();
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
3196
4065
|
setTimeout(function() {
|
|
3197
4066
|
var later = self.host.getIdentity();
|
|
3198
|
-
if (later.
|
|
4067
|
+
if (later.projectId !== svcId || later.platform !== platform) return;
|
|
3199
4068
|
if (self.isPollingPaused() || !self.host.isViewMounted()) return;
|
|
3200
4069
|
self._adoptWorkerIndexingPasses(attempt + 1);
|
|
3201
4070
|
}, WORKER_PASS_ADOPT_ATTEMPTS[attempt + 1]);
|
|
@@ -3237,7 +4106,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3237
4106
|
if (!ref || !ref.name) return false;
|
|
3238
4107
|
if (!this._isWorkerDrivenIndexing(ref.name, ref.mime)) return false;
|
|
3239
4108
|
this.bgTaskQueue.push({
|
|
3240
|
-
|
|
4109
|
+
projectId: svcId,
|
|
3241
4110
|
platform,
|
|
3242
4111
|
id: item.id,
|
|
3243
4112
|
filename: ref.name,
|
|
@@ -3268,8 +4137,8 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3268
4137
|
url,
|
|
3269
4138
|
method: "POST",
|
|
3270
4139
|
id: serverId,
|
|
3271
|
-
queue: bgIndexingQueueName(id.userId, id.
|
|
3272
|
-
service: id.
|
|
4140
|
+
queue: bgIndexingQueueName(id.userId, id.projectId),
|
|
4141
|
+
service: id.projectId,
|
|
3273
4142
|
owner: id.owner
|
|
3274
4143
|
})).catch(function() {
|
|
3275
4144
|
});
|
|
@@ -3278,7 +4147,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3278
4147
|
drainBgTaskQueue() {
|
|
3279
4148
|
var self = this;
|
|
3280
4149
|
var id = this.host.getIdentity();
|
|
3281
|
-
var svcId = id.
|
|
4150
|
+
var svcId = id.projectId, plat = id.platform;
|
|
3282
4151
|
if (!svcId || plat === "none" || !this.host.isViewMounted()) return;
|
|
3283
4152
|
this._applyIndexCancellations();
|
|
3284
4153
|
this._sweepCancelledIndexing();
|
|
@@ -3292,11 +4161,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3292
4161
|
});
|
|
3293
4162
|
for (var i = this.bgTaskQueue.length - 1; i >= 0; i--) {
|
|
3294
4163
|
var e = this.bgTaskQueue[i];
|
|
3295
|
-
if (e.
|
|
4164
|
+
if (e.projectId !== svcId || e.platform !== plat) continue;
|
|
3296
4165
|
if (presentIds[e.id] && !pendingIds[e.id]) this.bgTaskQueue.splice(i, 1);
|
|
3297
4166
|
}
|
|
4167
|
+
var bgPollBudget = MAX_CONCURRENT_BG_POLLS - this._countBgPolls();
|
|
4168
|
+
var injectedAny = false;
|
|
3298
4169
|
this.bgTaskQueue.forEach(function(entry) {
|
|
3299
|
-
if (entry.
|
|
4170
|
+
if (entry.projectId !== svcId || entry.platform !== plat) return;
|
|
3300
4171
|
if (!presentIds[entry.id]) {
|
|
3301
4172
|
var isRunning = entry.status === "running";
|
|
3302
4173
|
var userBubble = {
|
|
@@ -3317,16 +4188,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3317
4188
|
};
|
|
3318
4189
|
if (isRunning) userBubble.isPendingInProcess = true;
|
|
3319
4190
|
else userBubble.isPendingQueued = true;
|
|
3320
|
-
self.state.messages.
|
|
3321
|
-
|
|
3322
|
-
|
|
4191
|
+
var stageAt = self._stageIndex(self.state.messages, entry.stageId);
|
|
4192
|
+
var runningBubble = isRunning ? { role: "assistant", content: "", isPending: true, isPendingInProcess: true, isBackgroundTask: true, _serverItemId: entry.id } : null;
|
|
4193
|
+
if (stageAt === -1) {
|
|
4194
|
+
self.state.messages.push(userBubble);
|
|
4195
|
+
if (runningBubble) self.state.messages.push(runningBubble);
|
|
4196
|
+
} else if (runningBubble) {
|
|
4197
|
+
self.state.messages.splice(stageAt, 0, userBubble, runningBubble);
|
|
4198
|
+
} else {
|
|
4199
|
+
self.state.messages.splice(stageAt, 0, userBubble);
|
|
3323
4200
|
}
|
|
3324
4201
|
presentIds[entry.id] = true;
|
|
3325
|
-
|
|
3326
|
-
self.updateHistoryCache();
|
|
3327
|
-
self.host.scrollToBottomIfSticky(false);
|
|
4202
|
+
injectedAny = true;
|
|
3328
4203
|
}
|
|
3329
|
-
if (!self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
4204
|
+
if (bgPollBudget > 0 && !self.isPollingPaused() && !self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
4205
|
+
bgPollBudget--;
|
|
3330
4206
|
var capturedId = entry.id, capturedPlat = plat;
|
|
3331
4207
|
var capturedEntry = entry;
|
|
3332
4208
|
var wasStopped = false;
|
|
@@ -3359,15 +4235,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3359
4235
|
}
|
|
3360
4236
|
self.host.notify();
|
|
3361
4237
|
self.updateHistoryCache();
|
|
4238
|
+
if (!self._isWorkerDrivenIndexing(capturedEntry.filename, capturedEntry.mime)) {
|
|
4239
|
+
self._nudgeIndexingDrain();
|
|
4240
|
+
}
|
|
3362
4241
|
}).then(function() {
|
|
3363
4242
|
if (wasStopped) return;
|
|
3364
4243
|
var qi = self.bgTaskQueue.findIndex(function(q) {
|
|
3365
4244
|
return q.id === capturedId;
|
|
3366
4245
|
});
|
|
3367
4246
|
if (qi !== -1) self.bgTaskQueue.splice(qi, 1);
|
|
4247
|
+
self.drainBgTaskQueue();
|
|
3368
4248
|
});
|
|
3369
4249
|
}
|
|
3370
4250
|
});
|
|
4251
|
+
if (injectedAny) {
|
|
4252
|
+
this.host.notify();
|
|
4253
|
+
this.updateHistoryCache();
|
|
4254
|
+
this.host.scrollToBottomIfSticky(false);
|
|
4255
|
+
}
|
|
3371
4256
|
this.promoteNextBgQueuedToRunning();
|
|
3372
4257
|
}
|
|
3373
4258
|
// Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
|
|
@@ -3384,25 +4269,45 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3384
4269
|
// as well would now double-index every window.
|
|
3385
4270
|
maybeResumeIndexing(entry, response, platform) {
|
|
3386
4271
|
var self = this;
|
|
4272
|
+
var endOfClientChain = function() {
|
|
4273
|
+
self._nudgeIndexingDrain();
|
|
4274
|
+
};
|
|
3387
4275
|
try {
|
|
3388
4276
|
if (!entry || !entry.storagePath) return;
|
|
3389
4277
|
if (this.cancelledIndexKeys.has(this._indexKeyOf(entry))) return;
|
|
3390
|
-
if (!isPagedReadFile(entry.filename, entry.mime))
|
|
4278
|
+
if (!isPagedReadFile(entry.filename, entry.mime)) {
|
|
4279
|
+
endOfClientChain();
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
3391
4282
|
if (isImageVisionFile(entry.filename, entry.mime)) return;
|
|
3392
4283
|
if (windowedIndexingEnabled() && isWindowedReadFile(entry.filename, entry.mime)) return;
|
|
3393
|
-
if (isErrorResponseBody(response))
|
|
4284
|
+
if (isErrorResponseBody(response)) {
|
|
4285
|
+
endOfClientChain();
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
3394
4288
|
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
3395
|
-
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1)
|
|
4289
|
+
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) {
|
|
4290
|
+
endOfClientChain();
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
3396
4293
|
var pass = (entry.resumePass || 0) + 1;
|
|
3397
|
-
if (pass > MAX_INDEXING_RESUME_PASSES)
|
|
4294
|
+
if (pass > MAX_INDEXING_RESUME_PASSES) {
|
|
4295
|
+
endOfClientChain();
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
3398
4298
|
var id = this.host.getIdentity();
|
|
3399
|
-
if (!id || id.platform === "none" || id.
|
|
4299
|
+
if (!id || id.platform === "none" || id.projectId !== entry.projectId) return;
|
|
3400
4300
|
this.trackIndexDispatch(notifyAgentContinueIndexing({
|
|
3401
4301
|
platform: id.platform,
|
|
3402
4302
|
model: id.model,
|
|
3403
|
-
service: id.
|
|
4303
|
+
service: id.projectId,
|
|
4304
|
+
// Without this the resume pass rebuilds its system prompt from the RAW
|
|
4305
|
+
// regional id (requests.ts falls back to `service`), and the model copies
|
|
4306
|
+
// that id verbatim into project_id tool calls, which the MCP schema
|
|
4307
|
+
// pattern rejects - the whole continue pass saves nothing.
|
|
4308
|
+
publicProjectId: id.publicProjectId,
|
|
3404
4309
|
owner: id.owner,
|
|
3405
|
-
userId: id.userId || id.
|
|
4310
|
+
userId: id.userId || id.projectId,
|
|
3406
4311
|
serviceName: id.serviceName,
|
|
3407
4312
|
serviceDescription: id.serviceDescription,
|
|
3408
4313
|
attachment: {
|
|
@@ -3415,7 +4320,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3415
4320
|
}).then(function(ack) {
|
|
3416
4321
|
if (ack && typeof ack.id === "string") {
|
|
3417
4322
|
self.bgTaskQueue.push({
|
|
3418
|
-
|
|
4323
|
+
projectId: id.projectId,
|
|
3419
4324
|
platform: id.platform,
|
|
3420
4325
|
id: ack.id,
|
|
3421
4326
|
filename: entry.filename,
|
|
@@ -3426,6 +4331,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3426
4331
|
status: ack.status === "running" ? "running" : "pending",
|
|
3427
4332
|
poll: ack.poll,
|
|
3428
4333
|
resumePass: pass
|
|
4334
|
+
// Deliberately NOT stamped with entry.stageId. Only a batch's FIRST
|
|
4335
|
+
// pass anchors to the turn; a continuation appends, which is the
|
|
4336
|
+
// order the server queued it in and therefore the order
|
|
4337
|
+
// promoteNextBgQueuedToRunning should spin it in. It costs nothing
|
|
4338
|
+
// on screen: a continuation is folded into the run whose row
|
|
4339
|
+
// already sits above the turn, and renders nothing at its own
|
|
4340
|
+
// index (indexing_groups anchors a run at its FIRST loaded pass).
|
|
3429
4341
|
});
|
|
3430
4342
|
self.drainBgTaskQueue();
|
|
3431
4343
|
}
|
|
@@ -3444,21 +4356,25 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3444
4356
|
loadHistory(fetchMore, token) {
|
|
3445
4357
|
var self = this;
|
|
3446
4358
|
var id = this.host.getIdentity();
|
|
3447
|
-
var loadKey = !id.
|
|
4359
|
+
var loadKey = !id.projectId || id.platform === "none" ? "" : id.projectId + "#" + id.platform;
|
|
3448
4360
|
if (token === void 0) token = this.state.gateRefreshToken;
|
|
3449
|
-
if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.
|
|
4361
|
+
if (this.state.loadingHistory && this.state.historyRequestToken === token || id.platform === "none" || !id.projectId) {
|
|
3450
4362
|
return Promise.resolve();
|
|
3451
4363
|
}
|
|
3452
4364
|
this.state.historyRequestToken = token;
|
|
3453
4365
|
this.state.loadingHistory = true;
|
|
4366
|
+
if (!fetchMore && loadKey !== this._liveIndexKey) {
|
|
4367
|
+
this._liveIndexKey = loadKey;
|
|
4368
|
+
this._resetLiveIndexKeys();
|
|
4369
|
+
}
|
|
3454
4370
|
if (fetchMore) this.state.loadingOlderHistory = true;
|
|
3455
4371
|
this.host.notify();
|
|
3456
4372
|
var platform = id.platform;
|
|
3457
|
-
var
|
|
4373
|
+
var projectId = id.projectId, owner = id.owner;
|
|
3458
4374
|
var options = { fetchMore };
|
|
3459
4375
|
if (fetchMore && this.state.historyStartKeyHistory.length) options.startKeyHistory = this.state.historyStartKeyHistory.slice();
|
|
3460
4376
|
var fetchHistory = function() {
|
|
3461
|
-
return getChatHistory({ service:
|
|
4377
|
+
return getChatHistory({ service: projectId, owner, platform }, options);
|
|
3462
4378
|
};
|
|
3463
4379
|
return Promise.resolve().then(fetchHistory).catch(function(err) {
|
|
3464
4380
|
if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
|
|
@@ -3478,7 +4394,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3478
4394
|
});
|
|
3479
4395
|
var mapped = mapHistoryListToMessages(list, platform, {
|
|
3480
4396
|
clearedAt: self.host.getClearedAt(),
|
|
3481
|
-
|
|
4397
|
+
projectId: id.projectId,
|
|
3482
4398
|
formatIndexingLabel: self.host.formatIndexingLabel
|
|
3483
4399
|
}).messages;
|
|
3484
4400
|
var keptOlderPages = false;
|
|
@@ -3584,12 +4500,26 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3584
4500
|
self.updateHistoryCache();
|
|
3585
4501
|
self.host.notify();
|
|
3586
4502
|
if (!fetchMore) {
|
|
4503
|
+
var bgAllow = {};
|
|
4504
|
+
var bgHistBudget = MAX_CONCURRENT_BG_POLLS - self._countBgPolls();
|
|
4505
|
+
if (bgHistBudget > 0) {
|
|
4506
|
+
var bgIds = chatList.filter(function(it) {
|
|
4507
|
+
if (it.status !== "running" && it.status !== "pending") return false;
|
|
4508
|
+
if (!it.poll || !it.id) return false;
|
|
4509
|
+
if (!(it._isBgTask || it._isOnBgQueue)) return false;
|
|
4510
|
+
return !self.historyItemPolls.has(it.id);
|
|
4511
|
+
}).map(function(it) {
|
|
4512
|
+
return it.id;
|
|
4513
|
+
}).sort();
|
|
4514
|
+
for (var ba = 0; ba < bgIds.length && ba < bgHistBudget; ba++) bgAllow[bgIds[ba]] = true;
|
|
4515
|
+
}
|
|
3587
4516
|
chatList.forEach(function(item) {
|
|
3588
4517
|
if (item.status !== "running" && item.status !== "pending") return;
|
|
3589
4518
|
if (!item.poll || !item.id) return;
|
|
3590
4519
|
if (self.historyItemPolls.has(item.id)) return;
|
|
3591
4520
|
if (self.pendingAgentRequests[self.getHistoryCacheKey()] && !item._isBgTask && !item._isOnBgQueue) return;
|
|
3592
4521
|
if ((item._isBgTask || item._isOnBgQueue) && self.isPollingPaused()) return;
|
|
4522
|
+
if ((item._isBgTask || item._isOnBgQueue) && !bgAllow[item.id]) return;
|
|
3593
4523
|
var capturedId = item.id;
|
|
3594
4524
|
var pp = item.poll({
|
|
3595
4525
|
latency: POLL_INTERVAL,
|
|
@@ -3643,6 +4573,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3643
4573
|
});
|
|
3644
4574
|
self.drainBgTaskQueue();
|
|
3645
4575
|
}
|
|
4576
|
+
if (!fetchMore) self.refreshLiveIndexState();
|
|
3646
4577
|
if (!fetchMore) return self.host.scrollToBottomIfSticky();
|
|
3647
4578
|
}).catch(function(err) {
|
|
3648
4579
|
console.warn("[chat-engine] getChatHistory failed", err);
|
|
@@ -3660,7 +4591,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3660
4591
|
// Upload one attachment (a file = 1 member, a folder = N) to db storage and
|
|
3661
4592
|
// queue indexing per member. The bytes I/O + chip rendering go through host
|
|
3662
4593
|
// hooks; the overwrite/reindex flow, status lifecycle, and indexing live here.
|
|
3663
|
-
uploadSingleAttachment(att) {
|
|
4594
|
+
uploadSingleAttachment(att, stageId) {
|
|
3664
4595
|
var self = this;
|
|
3665
4596
|
var id = this.host.getIdentity();
|
|
3666
4597
|
att.status = "uploading";
|
|
@@ -3707,6 +4638,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3707
4638
|
return self.host.promptOverwrite(member.file.name).then(function(choice) {
|
|
3708
4639
|
if (choice === "overwrite") {
|
|
3709
4640
|
existedBefore = true;
|
|
4641
|
+
markImagePreviewStale(self.host.getIdentity().projectId || "default", member.storagePath);
|
|
3710
4642
|
return doMemberUpload(false);
|
|
3711
4643
|
}
|
|
3712
4644
|
if (choice === "skip") {
|
|
@@ -3727,17 +4659,39 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3727
4659
|
att.storagePath = member.storagePath;
|
|
3728
4660
|
}
|
|
3729
4661
|
var mime = member.file.type || self.host.getMimeType(member.file.name);
|
|
3730
|
-
var
|
|
3731
|
-
|
|
4662
|
+
var alreadyIndexing = false;
|
|
4663
|
+
var preIndex = self.claimIndexRun(member.storagePath).then(function(claimed) {
|
|
4664
|
+
alreadyIndexing = !claimed;
|
|
4665
|
+
if (alreadyIndexing) {
|
|
4666
|
+
console.log("[chat-engine] skipping a duplicate index request for", member.storagePath);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
if (existedBefore && typeof self.host.deleteExistingFileRecord === "function") {
|
|
4670
|
+
return Promise.resolve(self.host.deleteExistingFileRecord(member.storagePath)).catch(function() {
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
});
|
|
4674
|
+
preIndex = preIndex.then(function() {
|
|
4675
|
+
if (alreadyIndexing) return;
|
|
4676
|
+
if (typeof self.host.ensureFileIndexRecord !== "function") return;
|
|
4677
|
+
return Promise.resolve(self.host.ensureFileIndexRecord(member.storagePath, {
|
|
4678
|
+
name: member.file.name,
|
|
4679
|
+
mime: mime || void 0,
|
|
4680
|
+
size: member.file.size
|
|
4681
|
+
})).catch(function() {
|
|
4682
|
+
});
|
|
4683
|
+
});
|
|
3732
4684
|
return preIndex.then(function() {
|
|
3733
4685
|
return parseAttachmentContent(member.file, member.file.name, mime || void 0);
|
|
3734
4686
|
}).then(function(parsedContent) {
|
|
4687
|
+
if (alreadyIndexing) return;
|
|
3735
4688
|
return self.trackIndexDispatch(notifyAgentSaveAttachment({
|
|
3736
4689
|
platform: id.platform,
|
|
3737
4690
|
model: id.model,
|
|
3738
|
-
service: id.
|
|
4691
|
+
service: id.projectId,
|
|
4692
|
+
publicProjectId: id.publicProjectId,
|
|
3739
4693
|
owner: id.owner,
|
|
3740
|
-
userId: id.userId || id.
|
|
4694
|
+
userId: id.userId || id.projectId,
|
|
3741
4695
|
serviceName: id.serviceName,
|
|
3742
4696
|
serviceDescription: id.serviceDescription,
|
|
3743
4697
|
attachment: {
|
|
@@ -3751,7 +4705,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3751
4705
|
}).then(function(ack) {
|
|
3752
4706
|
if (ack && typeof ack.id === "string") {
|
|
3753
4707
|
self.bgTaskQueue.push({
|
|
3754
|
-
|
|
4708
|
+
projectId: id.projectId,
|
|
3755
4709
|
platform: id.platform,
|
|
3756
4710
|
id: ack.id,
|
|
3757
4711
|
filename: member.file.name,
|
|
@@ -3760,12 +4714,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3760
4714
|
mime: mime || void 0,
|
|
3761
4715
|
size: member.file.size,
|
|
3762
4716
|
status: ack.status === "running" ? "running" : "pending",
|
|
3763
|
-
poll: ack.poll
|
|
4717
|
+
poll: ack.poll,
|
|
4718
|
+
// Puts this file's row directly above the chat turn it was
|
|
4719
|
+
// attached to (drainBgTaskQueue). Undefined for an
|
|
4720
|
+
// attachment-only send, which appends.
|
|
4721
|
+
stageId
|
|
3764
4722
|
});
|
|
3765
4723
|
self.drainBgTaskQueue();
|
|
3766
4724
|
}
|
|
3767
4725
|
}, function(e) {
|
|
3768
4726
|
console.error("[chat-engine] indexing request failed", e);
|
|
4727
|
+
self.releaseIndexRun(member.storagePath);
|
|
3769
4728
|
anyIndexFailed = true;
|
|
3770
4729
|
if (!att.errorCode && !att.errorDetail) {
|
|
3771
4730
|
att.errorCode = e && (e.code || e.body && e.body.code) || "";
|
|
@@ -3797,7 +4756,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3797
4756
|
// message — uploading those here would attach them to the wrong turn, and
|
|
3798
4757
|
// collecting the previous batch's finished urls would attach files the user
|
|
3799
4758
|
// already sent. Omitted (no batch) means every chip, the old behavior.
|
|
3800
|
-
|
|
4759
|
+
//
|
|
4760
|
+
// `stageId` is the turn these chips were attached to, carried onto every indexing
|
|
4761
|
+
// task so its collapsed row renders directly ABOVE that turn's bubble (see
|
|
4762
|
+
// BgTaskEntry.stageId). Omitted for an attachment-only send, which has no turn.
|
|
4763
|
+
uploadPendingAttachments(batchId, stageId) {
|
|
3801
4764
|
var self = this;
|
|
3802
4765
|
this.host.resetOverwriteBatch();
|
|
3803
4766
|
this._uploadBatches += 1;
|
|
@@ -3826,7 +4789,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3826
4789
|
return;
|
|
3827
4790
|
}
|
|
3828
4791
|
}
|
|
3829
|
-
return self.uploadSingleAttachment(att).then(function(us) {
|
|
4792
|
+
return self.uploadSingleAttachment(att, stageId).then(function(us) {
|
|
3830
4793
|
collected.push.apply(collected, us);
|
|
3831
4794
|
}).catch(function(err) {
|
|
3832
4795
|
var removed = !self.state.attachments.some(function(a) {
|
|
@@ -3905,9 +4868,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3905
4868
|
function isPendingMsg(m) {
|
|
3906
4869
|
return !!(m.isPending || m.isPendingInProcess || m.isPendingQueued || m.isSendingToServer);
|
|
3907
4870
|
}
|
|
4871
|
+
function isHiddenPass(m) {
|
|
4872
|
+
if (m.role === "user") {
|
|
4873
|
+
if (m.isCancelled) return false;
|
|
4874
|
+
var ref = readFileRef(m);
|
|
4875
|
+
return !!(ref && ref.continued);
|
|
4876
|
+
}
|
|
4877
|
+
return !!m.isPending;
|
|
4878
|
+
}
|
|
3908
4879
|
function buildChatDisplayList(messages, opts) {
|
|
3909
4880
|
var list = Array.isArray(messages) ? messages : [];
|
|
4881
|
+
var liveIndexKeys = opts && opts.liveIndexKeys || {};
|
|
4882
|
+
var liveIndexChecked = !!(opts && opts.liveIndexChecked);
|
|
4883
|
+
var stoppedIndexIds = opts && opts.stoppedIndexIds || {};
|
|
4884
|
+
var windowedIndexing = opts && opts.windowedIndexing !== void 0 ? !!opts.windowedIndexing : windowedIndexingEnabled();
|
|
3910
4885
|
var hasMoreHistory = !!(opts && opts.hasMoreHistory);
|
|
4886
|
+
var loadingOlderHistory = !!(opts && opts.loadingOlderHistory);
|
|
3911
4887
|
var groups = {};
|
|
3912
4888
|
var order = [];
|
|
3913
4889
|
var runOfIndex = new Array(list.length);
|
|
@@ -3955,11 +4931,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3955
4931
|
status: "done",
|
|
3956
4932
|
cancellableIds: [],
|
|
3957
4933
|
cancelling: false,
|
|
4934
|
+
stopped: false,
|
|
3958
4935
|
mayHaveOlder: false,
|
|
3959
4936
|
// The run's first loaded pass, and never re-stamped: see the file
|
|
3960
4937
|
// docstring. `anchorId` is filled in once every member is known.
|
|
3961
4938
|
anchorIndex: i,
|
|
3962
|
-
anchorId: ""
|
|
4939
|
+
anchorId: "",
|
|
4940
|
+
// All five are derived once every member is known, below.
|
|
4941
|
+
visibleMembers: [],
|
|
4942
|
+
driver: "single",
|
|
4943
|
+
finished: false,
|
|
4944
|
+
resolving: false
|
|
3963
4945
|
};
|
|
3964
4946
|
order.push(runId);
|
|
3965
4947
|
}
|
|
@@ -3977,6 +4959,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3977
4959
|
if (msg._serverItemId) runByItemId[msg._serverItemId] = runId;
|
|
3978
4960
|
if (ref && ref.name) keyByName[ref.name] = g.key;
|
|
3979
4961
|
}
|
|
4962
|
+
var newestRunOfKey = {};
|
|
4963
|
+
for (var nk in runsOfKey) {
|
|
4964
|
+
var nrs = runsOfKey[nk];
|
|
4965
|
+
if (nrs.length) newestRunOfKey[nrs[nrs.length - 1]] = true;
|
|
4966
|
+
}
|
|
3980
4967
|
for (var rk in runsOfKey) {
|
|
3981
4968
|
var runIds = runsOfKey[rk];
|
|
3982
4969
|
for (var ri = 0; ri < runIds.length; ri++) {
|
|
@@ -4000,6 +4987,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4000
4987
|
break;
|
|
4001
4988
|
}
|
|
4002
4989
|
}
|
|
4990
|
+
var stopped = false;
|
|
4991
|
+
for (var ki = 0; ki < grp.members.length; ki++) {
|
|
4992
|
+
var km = grp.members[ki].msg;
|
|
4993
|
+
if (km.isCancelled) {
|
|
4994
|
+
stopped = true;
|
|
4995
|
+
break;
|
|
4996
|
+
}
|
|
4997
|
+
if (km._serverItemId && stoppedIndexIds[km._serverItemId]) {
|
|
4998
|
+
stopped = true;
|
|
4999
|
+
break;
|
|
5000
|
+
}
|
|
5001
|
+
}
|
|
5002
|
+
grp.stopped = stopped;
|
|
4003
5003
|
for (var xi = 0; xi < grp.members.length; xi++) {
|
|
4004
5004
|
if (grp.members[xi].msg._cancelling) {
|
|
4005
5005
|
grp.cancelling = true;
|
|
@@ -4009,7 +5009,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4009
5009
|
var seenIds = {};
|
|
4010
5010
|
for (var ci = 0; ci < grp.members.length; ci++) {
|
|
4011
5011
|
var cm = grp.members[ci].msg;
|
|
4012
|
-
if (cm._cancelError && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
|
|
5012
|
+
if (cm._cancelError && !stopped && (active || grp.cancelling)) grp.cancelError = cm._cancelError;
|
|
4013
5013
|
if (cm.role !== "user" || !cm._serverItemId || cm._cancelling || cm.isSendingToServer) continue;
|
|
4014
5014
|
if (!(cm.isPendingQueued || cm.isPendingInProcess)) continue;
|
|
4015
5015
|
if (ci < lastSettled) continue;
|
|
@@ -4019,9 +5019,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4019
5019
|
}
|
|
4020
5020
|
if (active) {
|
|
4021
5021
|
grp.status = "active";
|
|
5022
|
+
if (stopped) grp.cancelling = true;
|
|
5023
|
+
} else if (stopped) {
|
|
5024
|
+
grp.status = "cancelled";
|
|
4022
5025
|
} else {
|
|
4023
5026
|
var last = grp.members[grp.members.length - 1].msg;
|
|
4024
|
-
grp.status = last.isError ? "error" :
|
|
5027
|
+
grp.status = last.isError ? "error" : "done";
|
|
4025
5028
|
}
|
|
4026
5029
|
var sawFirstPass = false;
|
|
4027
5030
|
for (var pi = 0; pi < grp.members.length; pi++) {
|
|
@@ -4037,6 +5040,35 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4037
5040
|
var anchor = grp.members[0];
|
|
4038
5041
|
grp.anchorIndex = anchor.index;
|
|
4039
5042
|
grp.anchorId = anchor.msg._serverItemId || anchor.msg._localId || "";
|
|
5043
|
+
var sawComplete = false;
|
|
5044
|
+
for (var vi = 0; vi < grp.members.length; vi++) {
|
|
5045
|
+
var vm = grp.members[vi];
|
|
5046
|
+
if (vm.msg._indexComplete) sawComplete = true;
|
|
5047
|
+
if (!isHiddenPass(vm.msg)) grp.visibleMembers.push(vm);
|
|
5048
|
+
}
|
|
5049
|
+
grp.driver = !isPagedReadFile(grp.name, grp.mime) ? "single" : isImageVisionFile(grp.name, grp.mime) ? "worker" : windowedIndexing ? "worker" : "client";
|
|
5050
|
+
if (grp.status === "active") {
|
|
5051
|
+
grp.finished = false;
|
|
5052
|
+
} else if (grp.status === "cancelled") {
|
|
5053
|
+
grp.finished = true;
|
|
5054
|
+
} else if (grp.driver === "single") {
|
|
5055
|
+
grp.finished = true;
|
|
5056
|
+
} else if (grp.driver === "client") {
|
|
5057
|
+
grp.finished = sawComplete || grp.status === "error" || grp.passCount >= MAX_INDEXING_RESUME_PASSES;
|
|
5058
|
+
} else {
|
|
5059
|
+
grp.finished = !newestRunOfKey[order[oi]] || liveIndexChecked && !liveIndexKeys[grp.key];
|
|
5060
|
+
}
|
|
5061
|
+
if (grp.status !== "done") {
|
|
5062
|
+
grp.resolving = false;
|
|
5063
|
+
} else if (grp.mayHaveOlder && loadingOlderHistory && !liveIndexKeys[grp.key] && newestRunOfKey[order[oi]]) {
|
|
5064
|
+
grp.resolving = true;
|
|
5065
|
+
grp.resolvingReason = "history";
|
|
5066
|
+
} else if (!grp.finished && grp.driver === "worker" && !liveIndexChecked && !liveIndexKeys[grp.key]) {
|
|
5067
|
+
grp.resolving = true;
|
|
5068
|
+
grp.resolvingReason = "status";
|
|
5069
|
+
} else {
|
|
5070
|
+
grp.resolving = false;
|
|
5071
|
+
}
|
|
4040
5072
|
}
|
|
4041
5073
|
var out = [];
|
|
4042
5074
|
for (var j = 0; j < list.length; j++) {
|
|
@@ -4054,7 +5086,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4054
5086
|
(function() {
|
|
4055
5087
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
4056
5088
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
4057
|
-
var BQ_VERSION = "1.8.
|
|
5089
|
+
var BQ_VERSION = "1.8.5" ;
|
|
4058
5090
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
4059
5091
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
4060
5092
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -4209,7 +5241,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4209
5241
|
// current UserProfile or null
|
|
4210
5242
|
service: null,
|
|
4211
5243
|
// resolved service info ({ ai_agent, name, ... })
|
|
4212
|
-
|
|
5244
|
+
projectId: null,
|
|
4213
5245
|
owner: null,
|
|
4214
5246
|
theme: null,
|
|
4215
5247
|
// agent config (read-only, admin-provided)
|
|
@@ -4223,7 +5255,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4223
5255
|
// current view name
|
|
4224
5256
|
};
|
|
4225
5257
|
function skey(base) {
|
|
4226
|
-
return base + ":" + (S.
|
|
5258
|
+
return base + ":" + (S.projectId || "default");
|
|
4227
5259
|
}
|
|
4228
5260
|
function loadTheme() {
|
|
4229
5261
|
var stored = lsGet(SK.theme);
|
|
@@ -4270,7 +5302,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4270
5302
|
});
|
|
4271
5303
|
}
|
|
4272
5304
|
function loadServiceInfo() {
|
|
4273
|
-
S.
|
|
5305
|
+
S.projectId = S.skapi && (S.skapi.service || S.skapi.connection && S.skapi.connection.service) || S.projectId;
|
|
4274
5306
|
S.owner = S.skapi && (S.skapi.owner || S.skapi.connection && S.skapi.connection.owner) || S.owner;
|
|
4275
5307
|
return Promise.resolve().then(function() {
|
|
4276
5308
|
if (typeof S.skapi.getConnectionInfo === "function") return S.skapi.getConnectionInfo();
|
|
@@ -4278,7 +5310,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4278
5310
|
}).then(function(conn) {
|
|
4279
5311
|
if (S.opts && S.opts.dev) console.log("[bunnyquery] loadServiceInfo", conn);
|
|
4280
5312
|
if (conn) {
|
|
4281
|
-
S.
|
|
5313
|
+
S.projectId = conn.service || S.projectId;
|
|
4282
5314
|
S.owner = conn.owner || S.owner;
|
|
4283
5315
|
}
|
|
4284
5316
|
return conn;
|
|
@@ -5637,8 +6669,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5637
6669
|
// Rendered .bq-message nodes, indexed BY MESSAGE INDEX (sparse: a message
|
|
5638
6670
|
// folded into a collapsed indexing row has no node of its own).
|
|
5639
6671
|
messageEls: [],
|
|
5640
|
-
// Expanded background-indexing rows, keyed by
|
|
5641
|
-
//
|
|
6672
|
+
// Expanded background-indexing rows, keyed by FILE (group.key). Not by run:
|
|
6673
|
+
// runKey is renamed whenever an earlier pass of the run loads, which closed
|
|
6674
|
+
// a row the user had opened. Two runs of one file therefore share an open
|
|
6675
|
+
// state, which is the right reading of "show me this file's steps".
|
|
5642
6676
|
indexGroupsOpen: {},
|
|
5643
6677
|
messagesBox: null,
|
|
5644
6678
|
// .bq-messages element
|
|
@@ -5649,6 +6683,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5649
6683
|
stickToBottom: true,
|
|
5650
6684
|
loadingHistory: false,
|
|
5651
6685
|
loadingOlderHistory: false,
|
|
6686
|
+
// The viewport-fill LOOP is running (createHistoryFiller.onRunningChange).
|
|
6687
|
+
// Spans the gaps between its pages, where loadingOlderHistory keeps dropping
|
|
6688
|
+
// back to false; a collapsed indexing row that is still waiting for its own
|
|
6689
|
+
// earlier passes renders off this rather than flickering once per page.
|
|
6690
|
+
// View-side, not delegated to session.state: the filler is view-side too.
|
|
6691
|
+
historyFilling: false,
|
|
5652
6692
|
historyEndOfList: false,
|
|
5653
6693
|
historyStartKeyHistory: [],
|
|
5654
6694
|
historyRequestToken: 0,
|
|
@@ -5680,13 +6720,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5680
6720
|
var refreshingLinkMap = {};
|
|
5681
6721
|
var refreshedExpiredLinkMap = {};
|
|
5682
6722
|
var refreshingLinkPromises = /* @__PURE__ */ new Map();
|
|
6723
|
+
var unavailableLinkMap = {};
|
|
5683
6724
|
var fileBlobCache = /* @__PURE__ */ new Map();
|
|
5684
6725
|
var markedReady = null;
|
|
5685
6726
|
function currentIdentity() {
|
|
5686
6727
|
return {
|
|
5687
|
-
|
|
6728
|
+
projectId: S.projectId,
|
|
6729
|
+
// Prefer the SDK's formatted token. The widget takes the page's own
|
|
6730
|
+
// skapi-js <script> pin, and builds older than 1.8.4 have no .project_id;
|
|
6731
|
+
// compose the formatted token exactly as buildSystemPrompt does, because
|
|
6732
|
+
// the raw regional id must never reach the indexing prompt - the model
|
|
6733
|
+
// copies it verbatim into project_id tool calls, which the MCP schema
|
|
6734
|
+
// pattern rejects.
|
|
6735
|
+
publicProjectId: S.skapi && S.skapi.project_id || (function() {
|
|
6736
|
+
if (S.projectId && S.owner && S.skapi && S.skapi.util && typeof S.skapi.util.formatServiceId === "function") {
|
|
6737
|
+
try {
|
|
6738
|
+
return S.skapi.util.formatServiceId(S.projectId, S.owner);
|
|
6739
|
+
} catch (e) {
|
|
6740
|
+
}
|
|
6741
|
+
}
|
|
6742
|
+
return void 0;
|
|
6743
|
+
})(),
|
|
5688
6744
|
owner: S.owner,
|
|
5689
|
-
userId: S.user && S.user.user_id || S.
|
|
6745
|
+
userId: S.user && S.user.user_id || S.projectId,
|
|
5690
6746
|
platform: S.aiPlatform,
|
|
5691
6747
|
model: S.aiModel || void 0,
|
|
5692
6748
|
serviceName: S.serviceName,
|
|
@@ -5744,6 +6800,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5744
6800
|
deleteExistingFileRecord: function(path) {
|
|
5745
6801
|
return deleteFileIndexRecordDb(path);
|
|
5746
6802
|
},
|
|
6803
|
+
ensureFileIndexRecord: function(path, meta) {
|
|
6804
|
+
return ensureFileIndexRecordDb(path, meta);
|
|
6805
|
+
},
|
|
5747
6806
|
storagePathFor: function(relPath) {
|
|
5748
6807
|
return attachmentStoragePath(relPath);
|
|
5749
6808
|
},
|
|
@@ -5850,8 +6909,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5850
6909
|
return markedReady;
|
|
5851
6910
|
}
|
|
5852
6911
|
function buildSystemPrompt() {
|
|
6912
|
+
var promptProjectId = S.projectId || "";
|
|
6913
|
+
if (S.projectId && S.owner && S.skapi && S.skapi.util && typeof S.skapi.util.formatServiceId === "function") {
|
|
6914
|
+
try {
|
|
6915
|
+
promptProjectId = S.skapi.util.formatServiceId(S.projectId, S.owner);
|
|
6916
|
+
} catch (e) {
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
5853
6919
|
return buildChatSystemPrompt({
|
|
5854
|
-
|
|
6920
|
+
projectId: promptProjectId,
|
|
5855
6921
|
serviceName: S.serviceName,
|
|
5856
6922
|
serviceDescription: S.serviceDescription
|
|
5857
6923
|
});
|
|
@@ -5882,7 +6948,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5882
6948
|
});
|
|
5883
6949
|
}
|
|
5884
6950
|
function runAttachmentUpload(job) {
|
|
5885
|
-
return session.uploadPendingAttachments(job.batchId).then(function(attachmentUrls) {
|
|
6951
|
+
return session.uploadPendingAttachments(job.batchId, job.stageId).then(function(attachmentUrls) {
|
|
5886
6952
|
var failureGroups = groupAttachmentFailures(CS.attachments.filter(function(a) {
|
|
5887
6953
|
return a._batchId === job.batchId;
|
|
5888
6954
|
}));
|
|
@@ -5902,8 +6968,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5902
6968
|
}
|
|
5903
6969
|
function runAttachmentDispatch(job, attachmentUrls) {
|
|
5904
6970
|
if (!attachmentUrls || !job.text) return Promise.resolve();
|
|
5905
|
-
if (job.stageId) session.
|
|
6971
|
+
if (job.stageId) session.markStagedMessageIndexing(job.stageId);
|
|
5906
6972
|
return session.awaitIndexingDrained(job.pinned.identity).then(function() {
|
|
6973
|
+
if (job.stageId) session.markStagedMessageReady(job.stageId);
|
|
5907
6974
|
var c = composeUserMessage(job.text, attachmentUrls);
|
|
5908
6975
|
session.dispatchComposedMessage(c.composed, true, c.composedForLlm, c.extractContent, c.fileUrls, job.pinned);
|
|
5909
6976
|
});
|
|
@@ -5992,30 +7059,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5992
7059
|
var text = "\u2197 " + filename;
|
|
5993
7060
|
return '<a class="bq-file-download" href="' + escapeHtml(href) + '" download="' + escapeHtml(filename) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + "</a>";
|
|
5994
7061
|
}
|
|
5995
|
-
function linkToAnchorHtml(link) {
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
var attrs = [
|
|
6002
|
-
'class="' + cls.join(" ") + '"',
|
|
6003
|
-
'href="' + escapeHtml(link.href) + '"',
|
|
6004
|
-
'target="_blank"',
|
|
6005
|
-
'rel="noopener noreferrer"',
|
|
6006
|
-
'title="' + escapeHtml(link.fullLabel || link.label) + '"',
|
|
6007
|
-
'download="' + escapeHtml(link.fullLabel || link.label) + '"',
|
|
6008
|
-
'data-bq-link="1"'
|
|
6009
|
-
];
|
|
6010
|
-
if (link.expired) attrs.push('data-bq-expired="1"');
|
|
6011
|
-
if (link.expiredHref) attrs.push('data-bq-expired-href="' + escapeHtml(link.expiredHref) + '"');
|
|
6012
|
-
if (link.remotePath) attrs.push('data-bq-remote-path="' + escapeHtml(link.remotePath) + '"');
|
|
6013
|
-
if (link.fullLabel) attrs.push('data-bq-full-label="' + escapeHtml(link.fullLabel) + '"');
|
|
6014
|
-
return "<a " + attrs.join(" ") + ">" + escapeHtml(labelText) + "</a>";
|
|
7062
|
+
function linkToAnchorHtml(link, allowImagePreview) {
|
|
7063
|
+
return renderInlineLinkHtml(link, {
|
|
7064
|
+
refreshing: !!refreshingLinkMap[link.expiredHref || link.href],
|
|
7065
|
+
allowImagePreview,
|
|
7066
|
+
unavailable: isLinkUnavailable(link, unavailableLinkMap)
|
|
7067
|
+
});
|
|
6015
7068
|
}
|
|
6016
7069
|
function buildLinkPartFromGroups(full, g1, g2, g3, g4, g5, g6) {
|
|
6017
7070
|
return classifyInlineLink(full, [g1, g2, g3, g4, g5, g6], {
|
|
6018
|
-
|
|
7071
|
+
projectId: S.projectId,
|
|
6019
7072
|
dbHostPrefix: "https://db." + hostDomain(),
|
|
6020
7073
|
resolveFreshHref: function(expiredHref) {
|
|
6021
7074
|
return refreshedExpiredLinkMap[expiredHref];
|
|
@@ -6050,12 +7103,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6050
7103
|
codeMasks.push(match);
|
|
6051
7104
|
return "\uE002C" + idx + "\uE003";
|
|
6052
7105
|
});
|
|
7106
|
+
var previewsLeft = IMAGE_PREVIEWS_PER_MESSAGE;
|
|
6053
7107
|
var linkRe = createInlineLinkRegex();
|
|
6054
7108
|
working = working.replace(linkRe, function(full) {
|
|
6055
7109
|
var args = Array.prototype.slice.call(arguments, 1, 7);
|
|
6056
7110
|
var built = buildLinkPartFromGroups(full, args[0], args[1], args[2], args[3], args[4], args[5]);
|
|
6057
7111
|
if (!built) return full;
|
|
6058
|
-
|
|
7112
|
+
var allow = previewsLeft > 0;
|
|
7113
|
+
var html2 = linkToAnchorHtml(built.part, allow);
|
|
7114
|
+
if (allow && built.part.image) previewsLeft--;
|
|
7115
|
+
return pushPlaceholder(html2) + (built.tail || "");
|
|
6059
7116
|
});
|
|
6060
7117
|
working = working.replace(/C(\d+)/g, function(_m, idx) {
|
|
6061
7118
|
return codeMasks[Number(idx)] || "";
|
|
@@ -6139,7 +7196,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6139
7196
|
if (checkExistence === void 0) checkExistence = true;
|
|
6140
7197
|
var params = {
|
|
6141
7198
|
reserved_key: uploadReservedKey(),
|
|
6142
|
-
service: S.
|
|
7199
|
+
service: S.projectId,
|
|
6143
7200
|
owner: S.owner,
|
|
6144
7201
|
request: "db",
|
|
6145
7202
|
key: storagePath,
|
|
@@ -6157,22 +7214,52 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6157
7214
|
}
|
|
6158
7215
|
function deleteFileIndexRecordDb(storagePath) {
|
|
6159
7216
|
if (!storagePath || !S.skapi || typeof S.skapi.deleteRecords !== "function") return Promise.resolve();
|
|
6160
|
-
return S.skapi.deleteRecords({ service: S.
|
|
7217
|
+
return S.skapi.deleteRecords({ service: S.projectId, unique_id: "src::" + storagePath }).catch(function() {
|
|
6161
7218
|
});
|
|
6162
7219
|
}
|
|
6163
|
-
function
|
|
7220
|
+
function ensureFileIndexRecordDb(storagePath, meta) {
|
|
7221
|
+
if (!storagePath || !S.skapi || typeof S.skapi.postRecord !== "function") return Promise.resolve();
|
|
7222
|
+
return Promise.resolve(S.skapi.postRecord(null, {
|
|
7223
|
+
service: S.projectId,
|
|
7224
|
+
unique_id: "src::" + storagePath,
|
|
7225
|
+
table: { name: "file_summaries", access_group: "authorized" },
|
|
7226
|
+
// Deleting the file record must cascade to every record referencing it.
|
|
7227
|
+
source: { can_remove_referencing_records: true },
|
|
7228
|
+
data: {
|
|
7229
|
+
file_name: meta && meta.name || storagePath.split("/").pop() || storagePath,
|
|
7230
|
+
storage_path: storagePath,
|
|
7231
|
+
mime_type: meta && meta.mime || null,
|
|
7232
|
+
size_bytes: meta && typeof meta.size === "number" ? meta.size : null,
|
|
7233
|
+
indexed_at: Date.now(),
|
|
7234
|
+
note: "File-level record created at upload. The indexing agent enriches this with sheet names, column headers and row counts."
|
|
7235
|
+
}
|
|
7236
|
+
})).catch(function() {
|
|
7237
|
+
});
|
|
7238
|
+
}
|
|
7239
|
+
function getTemporaryUrlDb(path, expires, cdn, contentType, opts) {
|
|
7240
|
+
opts = opts || {};
|
|
6164
7241
|
var body = {
|
|
6165
|
-
service: S.
|
|
7242
|
+
service: S.projectId,
|
|
6166
7243
|
owner: S.owner,
|
|
6167
7244
|
request: "get-db",
|
|
6168
7245
|
key: path,
|
|
6169
7246
|
expires: expires || ATTACHMENT_URL_EXPIRES_SECONDS,
|
|
6170
|
-
contentType: mimeGetType(path) || "application/octet-stream"
|
|
7247
|
+
contentType: contentType || mimeGetType(path) || "application/octet-stream"
|
|
6171
7248
|
};
|
|
6172
7249
|
if (cdn !== false) body.generate_temporary_cdn_url = true;
|
|
6173
|
-
|
|
7250
|
+
var reqOpts = { auth: true, method: "post" };
|
|
7251
|
+
if (cdn === false && opts.browserCache) {
|
|
7252
|
+
reqOpts.method = "get";
|
|
7253
|
+
reqOpts.stableGateway = true;
|
|
7254
|
+
if (opts.refresh) reqOpts.revalidate = true;
|
|
7255
|
+
body.browser_cache = opts.browserCache;
|
|
7256
|
+
var uid = S.user && S.user.user_id;
|
|
7257
|
+
if (uid) body.uid = uid;
|
|
7258
|
+
}
|
|
7259
|
+
return S.skapi.util.request("get-signed-url", body, reqOpts).then(function(res) {
|
|
6174
7260
|
var u = typeof res === "string" ? res : res && res.url;
|
|
6175
7261
|
if (!u) throw new Error("No temporary URL returned.");
|
|
7262
|
+
if (/^https?:\/\//i.test(u)) return u;
|
|
6176
7263
|
return "https://db." + hostDomain() + "/" + u;
|
|
6177
7264
|
});
|
|
6178
7265
|
}
|
|
@@ -6567,6 +7654,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6567
7654
|
}
|
|
6568
7655
|
}
|
|
6569
7656
|
function uploadsFrozenForUser() {
|
|
7657
|
+
if (!S.user) return true;
|
|
6570
7658
|
var conf = S.service && S.service.conf || {};
|
|
6571
7659
|
if (!conf.freeze_database) return false;
|
|
6572
7660
|
var ag = S.user && typeof S.user.access_group === "number" ? S.user.access_group : 0;
|
|
@@ -6673,9 +7761,54 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6673
7761
|
if (!remotePath) return Promise.reject(new Error("Missing attachment path."));
|
|
6674
7762
|
return getTemporaryUrlDb(remotePath, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, false);
|
|
6675
7763
|
}
|
|
7764
|
+
var unavailableRepaintQueued = false;
|
|
7765
|
+
function markLinkUnavailable(key) {
|
|
7766
|
+
if (!key || unavailableLinkMap[key]) return;
|
|
7767
|
+
unavailableLinkMap[key] = true;
|
|
7768
|
+
if (!refreshedLinkExpiryTimer) scheduleNextLinkExpiryBoundary();
|
|
7769
|
+
if (unavailableRepaintQueued) return;
|
|
7770
|
+
unavailableRepaintQueued = true;
|
|
7771
|
+
setTimeout(function() {
|
|
7772
|
+
unavailableRepaintQueued = false;
|
|
7773
|
+
renderMessages();
|
|
7774
|
+
}, 0);
|
|
7775
|
+
}
|
|
7776
|
+
function imagePreviewCtx() {
|
|
7777
|
+
return {
|
|
7778
|
+
scope: S.projectId || "default",
|
|
7779
|
+
mint: function(remotePath, contentType, refresh) {
|
|
7780
|
+
return getTemporaryUrlDb(remotePath, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, false, contentType, {
|
|
7781
|
+
browserCache: PREVIEW_BROWSER_CACHE_SECONDS,
|
|
7782
|
+
refresh
|
|
7783
|
+
});
|
|
7784
|
+
},
|
|
7785
|
+
// An image arriving late pushes the conversation down under the
|
|
7786
|
+
// viewport. Re-pin only if the user was already at the bottom.
|
|
7787
|
+
onLoad: function() {
|
|
7788
|
+
scrollToBottomIfSticky(false);
|
|
7789
|
+
},
|
|
7790
|
+
// The mint was refused, or the url it minted would not load. Either
|
|
7791
|
+
// way there is no url for this file, so the caption chip left behind
|
|
7792
|
+
// must not keep offering a click that opens a dead tab.
|
|
7793
|
+
onError: function(path, err) {
|
|
7794
|
+
console.warn("[bunnyquery] image preview failed", path, err);
|
|
7795
|
+
markLinkUnavailable(linkUnavailableKeyForPath(path));
|
|
7796
|
+
}
|
|
7797
|
+
};
|
|
7798
|
+
}
|
|
7799
|
+
function hydrateMessageImagePreviews() {
|
|
7800
|
+
if (!CS.messagesBox) return;
|
|
7801
|
+
var nodes = CS.messagesBox.querySelectorAll("img.bq-img-preview:not([data-bq-img-state])");
|
|
7802
|
+
if (!nodes.length) return;
|
|
7803
|
+
var list = Array.prototype.filter.call(nodes, function(n) {
|
|
7804
|
+
return !(n.closest && n.closest(".bq-index-label"));
|
|
7805
|
+
});
|
|
7806
|
+
if (list.length) hydrateImagePreviews(list, imagePreviewCtx());
|
|
7807
|
+
}
|
|
6676
7808
|
var refreshedLinkExpiryTimer = null;
|
|
6677
7809
|
function expireAllRefreshedLinks() {
|
|
6678
7810
|
for (var k in refreshedExpiredLinkMap) delete refreshedExpiredLinkMap[k];
|
|
7811
|
+
for (var u in unavailableLinkMap) delete unavailableLinkMap[u];
|
|
6679
7812
|
}
|
|
6680
7813
|
function scheduleNextLinkExpiryBoundary() {
|
|
6681
7814
|
if (refreshedLinkExpiryTimer) clearTimeout(refreshedLinkExpiryTimer);
|
|
@@ -6694,7 +7827,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6694
7827
|
if (inFlight) return inFlight;
|
|
6695
7828
|
var run = (function() {
|
|
6696
7829
|
refreshingLinkMap[expiredHref] = true;
|
|
6697
|
-
var resolved = remotePath || extractRemotePathFromAttachmentHref(expiredHref, S.
|
|
7830
|
+
var resolved = remotePath || extractRemotePathFromAttachmentHref(expiredHref, S.projectId);
|
|
6698
7831
|
if (!resolved) return Promise.reject(new Error("Unable to refresh this expired attachment link."));
|
|
6699
7832
|
return getPublicTemporaryUrl(resolved).then(function(fresh) {
|
|
6700
7833
|
refreshedExpiredLinkMap[expiredHref] = fresh;
|
|
@@ -6721,6 +7854,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6721
7854
|
if (!target) return;
|
|
6722
7855
|
var anchor = target.closest ? target.closest("a[data-bq-link]") : null;
|
|
6723
7856
|
if (!anchor) return;
|
|
7857
|
+
if (anchor.dataset.bqUnavailable === "1") {
|
|
7858
|
+
e.preventDefault();
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
6724
7861
|
if (anchor.dataset.bqExpired !== "1") return;
|
|
6725
7862
|
e.preventDefault();
|
|
6726
7863
|
var originalHref = anchor.dataset.bqExpiredHref || anchor.href;
|
|
@@ -6738,12 +7875,13 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6738
7875
|
anchor.click();
|
|
6739
7876
|
}).catch(function(err) {
|
|
6740
7877
|
console.error("[bunnyquery] expired link refresh failed", err);
|
|
6741
|
-
|
|
7878
|
+
markLinkUnavailable(linkUnavailableKeyForHref(originalHref));
|
|
7879
|
+
if (anchor.dataset.bqRemotePath) markLinkUnavailable(linkUnavailableKeyForPath(anchor.dataset.bqRemotePath));
|
|
6742
7880
|
});
|
|
6743
7881
|
}
|
|
6744
7882
|
function getClearHistoryStorageKey() {
|
|
6745
|
-
if (!S.
|
|
6746
|
-
return SK.clearHorizon + ":" + S.
|
|
7883
|
+
if (!S.projectId || S.aiPlatform === "none") return "";
|
|
7884
|
+
return SK.clearHorizon + ":" + S.projectId + "#" + S.aiPlatform;
|
|
6747
7885
|
}
|
|
6748
7886
|
function getClearedAt() {
|
|
6749
7887
|
var key = getClearHistoryStorageKey();
|
|
@@ -6808,6 +7946,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6808
7946
|
},
|
|
6809
7947
|
fetchOlder: function() {
|
|
6810
7948
|
return fetchOlderHistoryIfNeeded();
|
|
7949
|
+
},
|
|
7950
|
+
// A collapsed indexing row whose run begins above the loaded window says
|
|
7951
|
+
// "loading" for as long as the pages that would complete it keep coming, and
|
|
7952
|
+
// that span is the LOOP, not a page (see createHistoryFiller). Rendering the
|
|
7953
|
+
// flip is safe from here: renderMessages never starts a fill of its own, and
|
|
7954
|
+
// the loop's own guard has already flipped before this is called.
|
|
7955
|
+
onRunningChange: function(running) {
|
|
7956
|
+
CS.historyFilling = running;
|
|
7957
|
+
renderMessages();
|
|
6811
7958
|
}
|
|
6812
7959
|
});
|
|
6813
7960
|
function pageOlderHistoryUntil(isSatisfied, token) {
|
|
@@ -6892,7 +8039,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6892
8039
|
if (msg.isError) cls.push("is-error");
|
|
6893
8040
|
if (msg.isCancelled) cls.push("is-cancelled");
|
|
6894
8041
|
if (msg.isPendingQueued || msg.isPendingOlder) cls.push("is-pending-older");
|
|
6895
|
-
if (msg.
|
|
8042
|
+
if (msg._dimSending || msg._cancelling) cls.push("is-sending-to-server");
|
|
6896
8043
|
var bubble;
|
|
6897
8044
|
if (msg.isPending) {
|
|
6898
8045
|
bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }));
|
|
@@ -6916,6 +8063,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6916
8063
|
md.addEventListener("click", onBubbleLinkClick);
|
|
6917
8064
|
bubble.appendChild(md);
|
|
6918
8065
|
if (msg.isUploadingAttachments) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Uploading files...)" }));
|
|
8066
|
+
else if (msg.isAwaitingIndexing) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Indexing files...)" }));
|
|
6919
8067
|
else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
6920
8068
|
if (msg.isCancelled) bubble.appendChild(h("span", { class: "bq-cancel-error", text: "(cancelled)" }));
|
|
6921
8069
|
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
@@ -6928,16 +8076,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6928
8076
|
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>';
|
|
6929
8077
|
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>';
|
|
6930
8078
|
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>';
|
|
8079
|
+
var INDEX_ICON_PENDING = '<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.4V12l3.3 2"/></svg>';
|
|
6931
8080
|
function indexGroupIcon(group) {
|
|
6932
|
-
if (group.status === "active") return INDEX_ICON_ACTIVE;
|
|
6933
|
-
if (group.status === "error") return INDEX_ICON_ERROR;
|
|
6934
8081
|
if (group.status === "cancelled") return INDEX_ICON_CANCELLED;
|
|
8082
|
+
if (group.resolving) return INDEX_ICON_PENDING;
|
|
8083
|
+
if (!group.finished) return INDEX_ICON_ACTIVE;
|
|
8084
|
+
if (group.status === "error") return INDEX_ICON_ERROR;
|
|
6935
8085
|
return INDEX_ICON_DONE;
|
|
6936
8086
|
}
|
|
6937
8087
|
function indexGroupVerb(group) {
|
|
6938
|
-
if (group.status === "active") return group.isReindex ? "Reindexing" : "Indexing";
|
|
6939
|
-
if (group.status === "error") return "Indexing failed:";
|
|
6940
8088
|
if (group.status === "cancelled") return "Indexing cancelled:";
|
|
8089
|
+
if (group.resolving) {
|
|
8090
|
+
return group.resolvingReason === "history" ? "Loading history:" : "Checking status:";
|
|
8091
|
+
}
|
|
8092
|
+
if (!group.finished) return group.isReindex ? "Reindexing" : "Indexing";
|
|
8093
|
+
if (group.status === "error") return "Indexing failed:";
|
|
6941
8094
|
return group.isReindex ? "Reindexed" : "Indexed";
|
|
6942
8095
|
}
|
|
6943
8096
|
function indexGroupLabel(group) {
|
|
@@ -6948,6 +8101,102 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6948
8101
|
if (group.passCount <= 1 && !group.mayHaveOlder) return "";
|
|
6949
8102
|
return group.passCount + (group.mayHaveOlder ? "+" : "") + " passes";
|
|
6950
8103
|
}
|
|
8104
|
+
function displayListOptions() {
|
|
8105
|
+
var liveIndex = session.getLiveIndexState();
|
|
8106
|
+
return {
|
|
8107
|
+
hasMoreHistory: !CS.historyEndOfList,
|
|
8108
|
+
// Older pages coming in RIGHT NOW. CS.historyFilling, not just the
|
|
8109
|
+
// per-request flag: the viewport fill is many pages and that flag drops
|
|
8110
|
+
// to false between every one of them, which is once per page of flicker
|
|
8111
|
+
// on any row rendered off it.
|
|
8112
|
+
loadingOlderHistory: !!(CS.loadingOlderHistory || CS.historyFilling),
|
|
8113
|
+
liveIndexKeys: liveIndex.keys,
|
|
8114
|
+
liveIndexChecked: liveIndex.checked,
|
|
8115
|
+
// Runs the user stopped. A stop that landed on a RUNNING pass leaves no
|
|
8116
|
+
// cancelled bubble behind (that pass finishes and answers normally), so
|
|
8117
|
+
// without this the row reports the stop as a finished "Indexed".
|
|
8118
|
+
stoppedIndexIds: session.getStoppedIndexIds()
|
|
8119
|
+
};
|
|
8120
|
+
}
|
|
8121
|
+
var stopIndexState = { runKey: "", fileKey: "", handle: null };
|
|
8122
|
+
function indexGroupStoppable(group) {
|
|
8123
|
+
return !!group && !group.finished && !group.resolving && !group.stopped && !group.cancelling;
|
|
8124
|
+
}
|
|
8125
|
+
function findCancellableIndexGroup(runKey, fileKey) {
|
|
8126
|
+
if (!runKey) return null;
|
|
8127
|
+
var list = buildChatDisplayList(CS.messages, displayListOptions());
|
|
8128
|
+
var byFile = null;
|
|
8129
|
+
for (var i = 0; i < list.length; i++) {
|
|
8130
|
+
var row = list[i];
|
|
8131
|
+
if (row.kind !== "indexing") continue;
|
|
8132
|
+
var live = indexGroupStoppable(row.group) ? row.group : null;
|
|
8133
|
+
if (row.group.runKey === runKey) return live;
|
|
8134
|
+
if (!byFile && live && fileKey && row.group.key === fileKey) byFile = row.group;
|
|
8135
|
+
}
|
|
8136
|
+
return byFile;
|
|
8137
|
+
}
|
|
8138
|
+
function stopIndexModalIsOpen() {
|
|
8139
|
+
var hnd = stopIndexState.handle;
|
|
8140
|
+
if (hnd && hnd.root && hnd.root.parentNode) return true;
|
|
8141
|
+
stopIndexState.runKey = "";
|
|
8142
|
+
stopIndexState.fileKey = "";
|
|
8143
|
+
stopIndexState.handle = null;
|
|
8144
|
+
return false;
|
|
8145
|
+
}
|
|
8146
|
+
function closeStopIndexModal() {
|
|
8147
|
+
var hnd = stopIndexState.handle;
|
|
8148
|
+
stopIndexState.runKey = "";
|
|
8149
|
+
stopIndexState.fileKey = "";
|
|
8150
|
+
stopIndexState.handle = null;
|
|
8151
|
+
if (hnd) hnd.close();
|
|
8152
|
+
}
|
|
8153
|
+
function syncStopIndexModal() {
|
|
8154
|
+
if (!stopIndexModalIsOpen()) return;
|
|
8155
|
+
if (!findCancellableIndexGroup(stopIndexState.runKey, stopIndexState.fileKey)) closeStopIndexModal();
|
|
8156
|
+
}
|
|
8157
|
+
function openStopIndexModal(group) {
|
|
8158
|
+
if (!indexGroupStoppable(group)) return;
|
|
8159
|
+
closeStopIndexModal();
|
|
8160
|
+
stopIndexState.runKey = group.runKey;
|
|
8161
|
+
stopIndexState.fileKey = group.key;
|
|
8162
|
+
var name = group.name;
|
|
8163
|
+
stopIndexState.handle = openModal(function(close) {
|
|
8164
|
+
var stopBtn = h("button", { class: "btn btn--danger", type: "button" }, "Stop indexing");
|
|
8165
|
+
stopBtn.addEventListener("click", function() {
|
|
8166
|
+
var runKey = stopIndexState.runKey;
|
|
8167
|
+
var fileKey = stopIndexState.fileKey;
|
|
8168
|
+
closeStopIndexModal();
|
|
8169
|
+
var live = findCancellableIndexGroup(runKey, fileKey);
|
|
8170
|
+
if (live) session.cancelIndexingGroup(live);
|
|
8171
|
+
});
|
|
8172
|
+
return h(
|
|
8173
|
+
"div",
|
|
8174
|
+
{ class: "bq-modal" },
|
|
8175
|
+
h("button", { class: "bq-modal-close", type: "button", html: "×", onclick: close }),
|
|
8176
|
+
h("div", { class: "bq-modal-delete-header" }, h("span", { text: "Stop indexing" })),
|
|
8177
|
+
// The file name is user data: translate="no" keeps a browser
|
|
8178
|
+
// translator from rewriting it (agent.vue tags it the same way).
|
|
8179
|
+
h(
|
|
8180
|
+
"p",
|
|
8181
|
+
{ class: "bq-modal-desc" },
|
|
8182
|
+
"Stop indexing \u201C",
|
|
8183
|
+
h("span", { translate: "no", text: name }),
|
|
8184
|
+
"\u201D?"
|
|
8185
|
+
),
|
|
8186
|
+
h(
|
|
8187
|
+
"p",
|
|
8188
|
+
{ class: "bq-modal-desc bq-modal-delete-warn" },
|
|
8189
|
+
"Whatever has been indexed so far stays searchable, and the pass already running finishes on the server. The remaining passes are dropped, not paused, so the file stays partly indexed until you reindex it."
|
|
8190
|
+
),
|
|
8191
|
+
h(
|
|
8192
|
+
"div",
|
|
8193
|
+
{ class: "bq-modal-btns" },
|
|
8194
|
+
h("button", { class: "btn btn--outline", type: "button", onclick: close }, "Keep indexing"),
|
|
8195
|
+
stopBtn
|
|
8196
|
+
)
|
|
8197
|
+
);
|
|
8198
|
+
});
|
|
8199
|
+
}
|
|
6951
8200
|
function toggleIndexGroup(key) {
|
|
6952
8201
|
if (CS.indexGroupsOpen[key]) delete CS.indexGroupsOpen[key];
|
|
6953
8202
|
else CS.indexGroupsOpen[key] = true;
|
|
@@ -6956,8 +8205,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6956
8205
|
}
|
|
6957
8206
|
function buildIndexGroupEl(group, isOpen) {
|
|
6958
8207
|
var cls = ["bq-index-group"];
|
|
6959
|
-
if (group.
|
|
6960
|
-
if (group.status
|
|
8208
|
+
if (group.resolving) cls.push("is-resolving");
|
|
8209
|
+
if (!group.resolving && !group.finished && group.status !== "cancelled") cls.push("is-active");
|
|
8210
|
+
if (!group.resolving && group.finished && group.status === "done") cls.push("is-indexed");
|
|
8211
|
+
if (group.finished && group.status === "error") cls.push("is-error");
|
|
6961
8212
|
if (isOpen) cls.push("is-open");
|
|
6962
8213
|
var label = h("span", { class: "bq-index-label", html: parseMsgPartsHtml(indexGroupLabel(group)) });
|
|
6963
8214
|
label.addEventListener("click", function(e) {
|
|
@@ -6965,7 +8216,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6965
8216
|
onBubbleLinkClick(e);
|
|
6966
8217
|
});
|
|
6967
8218
|
var cancelBtn = null;
|
|
6968
|
-
if (group
|
|
8219
|
+
if (indexGroupStoppable(group) || group.cancelling) {
|
|
6969
8220
|
cancelBtn = h("button", {
|
|
6970
8221
|
class: "bq-index-cancel" + (group.cancelling ? " is-disabled" : ""),
|
|
6971
8222
|
type: "button",
|
|
@@ -6976,7 +8227,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6976
8227
|
if (group.cancelling) cancelBtn.disabled = true;
|
|
6977
8228
|
else cancelBtn.addEventListener("click", function(e) {
|
|
6978
8229
|
e.stopPropagation();
|
|
6979
|
-
|
|
8230
|
+
openStopIndexModal(group);
|
|
6980
8231
|
});
|
|
6981
8232
|
cancelBtn.addEventListener("keydown", function(e) {
|
|
6982
8233
|
e.stopPropagation();
|
|
@@ -6991,12 +8242,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6991
8242
|
"aria-expanded": isOpen ? "true" : "false",
|
|
6992
8243
|
title: isOpen ? "Hide indexing steps" : "Show indexing steps",
|
|
6993
8244
|
onclick: function() {
|
|
6994
|
-
toggleIndexGroup(group.
|
|
8245
|
+
toggleIndexGroup(group.key);
|
|
6995
8246
|
},
|
|
6996
8247
|
onkeydown: function(e) {
|
|
6997
8248
|
if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
|
|
6998
8249
|
e.preventDefault();
|
|
6999
|
-
toggleIndexGroup(group.
|
|
8250
|
+
toggleIndexGroup(group.key);
|
|
7000
8251
|
}
|
|
7001
8252
|
},
|
|
7002
8253
|
h("span", { class: "bq-index-icon", html: indexGroupIcon(group) }),
|
|
@@ -7015,7 +8266,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7015
8266
|
if (isOpen && group.mayHaveOlder) {
|
|
7016
8267
|
el.appendChild(h("div", {
|
|
7017
8268
|
class: "bq-index-note",
|
|
7018
|
-
text: "Earlier passes of this file are further back in the conversation. Scroll up to load them."
|
|
8269
|
+
text: "Earlier passes of this file are further back in the conversation. " + (group.resolvingReason === "history" ? "Loading them now." : "Scroll up to load them.")
|
|
7019
8270
|
}));
|
|
7020
8271
|
}
|
|
7021
8272
|
return el;
|
|
@@ -7079,6 +8330,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7079
8330
|
box.scrollTop = anchor.scrollTop;
|
|
7080
8331
|
}
|
|
7081
8332
|
function renderMessages() {
|
|
8333
|
+
syncStopIndexModal();
|
|
7082
8334
|
if (!CS.messagesBox) return;
|
|
7083
8335
|
if (CS.chatSettingsOpen) return;
|
|
7084
8336
|
var anchor = captureScrollAnchor();
|
|
@@ -7102,22 +8354,28 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7102
8354
|
CS.messagesBox.appendChild(greet);
|
|
7103
8355
|
return;
|
|
7104
8356
|
}
|
|
7105
|
-
var rows = buildChatDisplayList(CS.messages,
|
|
8357
|
+
var rows = buildChatDisplayList(CS.messages, displayListOptions());
|
|
7106
8358
|
rows.forEach(function(row) {
|
|
7107
8359
|
if (row.kind === "indexing") {
|
|
7108
|
-
var isOpen = !!CS.indexGroupsOpen[row.group.
|
|
8360
|
+
var isOpen = !!CS.indexGroupsOpen[row.group.key];
|
|
7109
8361
|
var groupEl = buildIndexGroupEl(row.group, isOpen);
|
|
7110
8362
|
groupEl.setAttribute("data-row-key", "g" + row.group.runKey);
|
|
7111
8363
|
groupEl.setAttribute("data-row-pos", indexGroupAnchorId(row.group));
|
|
7112
8364
|
CS.messagesBox.appendChild(groupEl);
|
|
7113
8365
|
if (!isOpen) return;
|
|
7114
|
-
row.group.
|
|
8366
|
+
row.group.visibleMembers.forEach(function(member) {
|
|
7115
8367
|
var pass = buildMessageEl(member.msg, member.index);
|
|
7116
8368
|
pass.classList.add("bq-index-pass");
|
|
7117
8369
|
pass.setAttribute("data-row-key", rowAnchorKey(member.msg, member.index));
|
|
7118
8370
|
CS.messageEls[member.index] = pass;
|
|
7119
8371
|
CS.messagesBox.appendChild(pass);
|
|
7120
8372
|
});
|
|
8373
|
+
if (!row.group.finished && !row.group.resolving && row.group.status !== "cancelled") {
|
|
8374
|
+
CS.messagesBox.appendChild(h("div", {
|
|
8375
|
+
class: "bq-index-pass bq-index-tail",
|
|
8376
|
+
"aria-hidden": "true"
|
|
8377
|
+
}, h("span", { class: "bq-loader" })));
|
|
8378
|
+
}
|
|
7121
8379
|
return;
|
|
7122
8380
|
}
|
|
7123
8381
|
var el = buildMessageEl(row.msg, row.index);
|
|
@@ -7126,6 +8384,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7126
8384
|
CS.messagesBox.appendChild(el);
|
|
7127
8385
|
});
|
|
7128
8386
|
restoreScrollAnchor(anchor);
|
|
8387
|
+
hydrateMessageImagePreviews();
|
|
7129
8388
|
}
|
|
7130
8389
|
function refreshMessageBubble(idx) {
|
|
7131
8390
|
if (idx < 0 || idx >= CS.messages.length) return;
|
|
@@ -7135,8 +8394,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7135
8394
|
if (oldEl.classList.contains("bq-index-pass")) newEl.classList.add("bq-index-pass");
|
|
7136
8395
|
oldEl.parentNode.replaceChild(newEl, oldEl);
|
|
7137
8396
|
CS.messageEls[idx] = newEl;
|
|
8397
|
+
hydrateMessageImagePreviews();
|
|
7138
8398
|
}
|
|
7139
8399
|
function renderChat() {
|
|
8400
|
+
clearImagePreviewCache(S.projectId || "default");
|
|
8401
|
+
for (var uk in unavailableLinkMap) delete unavailableLinkMap[uk];
|
|
7140
8402
|
CS.messages = [];
|
|
7141
8403
|
CS.messageEls = [];
|
|
7142
8404
|
CS.indexGroupsOpen = {};
|
|
@@ -7409,7 +8671,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
7409
8671
|
var parsed = parseAiAgentValue2(raw);
|
|
7410
8672
|
S.aiPlatform = parsed.platform;
|
|
7411
8673
|
S.aiModel = parsed.model;
|
|
7412
|
-
setProjectContextWindow(S.
|
|
8674
|
+
setProjectContextWindow(S.projectId, parseAiAgentValue(raw).contextWindow);
|
|
7413
8675
|
S.serviceName = conn.service_name || "";
|
|
7414
8676
|
S.serviceDescription = conn.service_description || "";
|
|
7415
8677
|
}
|