bunnyquery 1.8.2 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.d.mts CHANGED
@@ -173,19 +173,19 @@ interface AttachmentFailureGroup {
173
173
  declare function groupAttachmentFailures(attachments: any[]): AttachmentFailureGroup[];
174
174
 
175
175
  /**
176
- * BASE PROMPT Chat assistant
176
+ * BASE PROMPT - Chat assistant
177
177
  * ============================================================================
178
178
  * System prompt sent on every chat turn. Rebuilt fresh on every send because
179
179
  * the project name/description can change at any time.
180
180
  *
181
181
  * The `${...}` placeholders are filled from the live project (service):
182
- * formattedServiceId -> the project ID the assistant is scoped to
183
- * serviceName -> project display name (only added if a description exists)
184
- * serviceDescription -> project description (only added if present)
182
+ * projectId -> the project ID the assistant is scoped to
183
+ * serviceName -> project display name (only added if a description exists)
184
+ * serviceDescription -> project description (only added if present)
185
185
  */
186
186
  type ChatSystemPromptParams = {
187
187
  /** The project/service ID this assistant is scoped to (formatted form). */
188
- formattedServiceId: string;
188
+ projectId: string;
189
189
  /** Project display name. Only appended when a description is also present. */
190
190
  serviceName?: string;
191
191
  /** Project description. When present, name + description are appended. */
@@ -194,7 +194,7 @@ type ChatSystemPromptParams = {
194
194
  declare function buildChatSystemPrompt(params: ChatSystemPromptParams): string;
195
195
 
196
196
  /**
197
- * BASE PROMPT Background file-indexing agent (system prompt)
197
+ * BASE PROMPT - Background file-indexing agent (system prompt)
198
198
  * ============================================================================
199
199
  * System prompt for the BACKGROUND indexing agent (notifyAgentSaveAttachment).
200
200
  * Its only job is to read the freshly uploaded file and persist what it learns
@@ -202,8 +202,8 @@ declare function buildChatSystemPrompt(params: ChatSystemPromptParams): string;
202
202
  * user-message template in ./indexing_user_message.ts.
203
203
  */
204
204
  type IndexingSystemPromptParams = {
205
- /** The project/service ID being indexed into. */
206
- service: string;
205
+ /** The PUBLIC project ID being indexed into (formatted token; the form the MCP tools accept). */
206
+ projectId: string;
207
207
  /** Project display name. Only appended when a description is also present. */
208
208
  serviceName?: string;
209
209
  /** Project description. When present, name + description are appended. */
@@ -212,14 +212,14 @@ type IndexingSystemPromptParams = {
212
212
  declare function buildIndexingSystemPrompt(params: IndexingSystemPromptParams): string;
213
213
 
214
214
  /**
215
- * BASE PROMPT Background file-indexing agent (user message)
215
+ * BASE PROMPT - Background file-indexing agent (user message)
216
216
  * ============================================================================
217
217
  * USER-role message paired with the indexing system prompt. Sent by
218
218
  * notifyAgentSaveAttachment() each time a file is uploaded or re-indexed.
219
219
  *
220
220
  * NOTE: the leading line "A new file has just been uploaded. Index it now." and
221
221
  * the "- name: ..." line are also what the chat client parses to build the
222
- * "Indexing: <name>" history bubble keep those fields on their own lines.
222
+ * "Indexing: <name>" history bubble - keep those fields on their own lines.
223
223
  */
224
224
  type IndexingAttachmentInfo = {
225
225
  /** Original file name. */
@@ -235,15 +235,15 @@ type IndexingAttachmentInfo = {
235
235
  };
236
236
  type BuildIndexingUserMessageOptions = {
237
237
  /**
238
- * For office files (.docx/.xlsx/.pptx) the model can't read the binary via
238
+ * For files with no paged reader (.epub/.hwp/.doc/.rtf, source code) the model can't read the binary via
239
239
  * web_fetch, so the proxy worker extracts the text server-side and replaces
240
240
  * this exact token with it. When provided, the message embeds the token (and
241
- * drops the temporary-URL line there is nothing for the model to fetch).
241
+ * drops the temporary-URL line - there is nothing for the model to fetch).
242
242
  */
243
243
  inlineContentPlaceholder?: string;
244
244
  /**
245
245
  * Actual file content parsed CLIENT-SIDE by an attachment-parser plugin (e.g.
246
- * an .hwp parser). Embedded inline verbatim no server extraction and no
246
+ * an .hwp parser). Embedded inline verbatim - no server extraction and no
247
247
  * web_fetch for this file. Takes precedence over `inlineContentPlaceholder`.
248
248
  */
249
249
  inlineContent?: string;
@@ -321,8 +321,8 @@ declare function registerModelContextWindows(models: Array<{
321
321
  id?: string;
322
322
  max_input_tokens?: number;
323
323
  }> | null | undefined): void;
324
- declare function setProjectContextWindow(serviceId: string, tokens: number | null | undefined): void;
325
- declare function getProjectContextWindow(serviceId: string): number | null;
324
+ declare function setProjectContextWindow(projectId: string, tokens: number | null | undefined): void;
325
+ declare function getProjectContextWindow(projectId: string): number | null;
326
326
  declare var OUTPUT_TOKEN_RESERVE: number;
327
327
  declare var TOOL_AND_RESPONSE_BUFFER: number;
328
328
  declare var MIN_INPUT_TOKEN_BUDGET: number;
@@ -349,7 +349,7 @@ declare function estimateMessageTokens(msg: {
349
349
  * such as 'claude-opus-4-7-20260101' resolves via 'claude-opus-4-7'. The walk
350
350
  * stops at the first hit, so a more specific entry always wins over its family.
351
351
  */
352
- declare function getContextWindow(platform: string, model?: string, serviceId?: string): number;
352
+ declare function getContextWindow(platform: string, model?: string, projectId?: string): number;
353
353
  declare function stripFileBlocksFromHistory(content: string): string;
354
354
  type BoundedChatOptions = {
355
355
  platform: string;
@@ -360,7 +360,7 @@ type BoundedChatOptions = {
360
360
  content: string;
361
361
  }>;
362
362
  /** Used to strip/rewrite expired attachment links in older user turns. */
363
- serviceId: string;
363
+ projectId: string;
364
364
  };
365
365
  declare function buildBoundedChatMessages(options: BoundedChatOptions): {
366
366
  messages: {
@@ -470,7 +470,7 @@ declare function prepareDownloadText(filename: string, body: string): {
470
470
 
471
471
  /**
472
472
  * Pure link/path helpers (no DOM, no marked). Moved verbatim from the chatbox.
473
- * `serviceId` is passed as a PARAMETER (the original read it from a global) so
473
+ * `projectId` is passed as a PARAMETER (the original read it from a global) so
474
474
  * the engine stays consumer-agnostic. The HTML-emitting helpers
475
475
  * (buildLinkPartFromGroups, linkToAnchorHtml, fileToAnchorHtml, parseMsgParts*)
476
476
  * stay in each VIEW — only these pure pieces move here.
@@ -488,6 +488,33 @@ declare var LINK_LABEL_MAX_DISPLAY_CHARS: number;
488
488
  * not, which is precisely the kind of divergence a shared constant exists to stop.
489
489
  */
490
490
  declare var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS: number;
491
+ /**
492
+ * Seconds the browser may reuse a minted preview url (`browser_cache`).
493
+ *
494
+ * A presigned url is a fresh SigV4 query string on every mint, so it can never
495
+ * be a browser cache key on its own and every reload re-downloads every image.
496
+ * Asking for the MINT with a cacheable GET fixes it from the other end: the same
497
+ * url comes back out of the browser cache, so the body already on disk stays
498
+ * addressable.
499
+ *
500
+ * Deliberately far longer than EXPIRED_LINK_REFRESH_EXPIRES_SECONDS above, and
501
+ * that is the whole trick: the url is short-lived while the file stays available
502
+ * locally for a WEEK. What keeps an image painting is the cached BODY, not a live
503
+ * url. Once the browser evicts that body it refetches with a url that has since
504
+ * expired, gets a 403, and the error path re-mints with `refresh`. That path is
505
+ * therefore load-bearing, not a rare fallback.
506
+ *
507
+ * A week is the platform default for reading a private file, not a number chosen
508
+ * here: skapi-js reads every private record file with
509
+ * PRIVATE_FILE_BROWSER_CACHE_SECONDS = 7 days against the same 20-minute url, and
510
+ * get_signed_url caps the header at BROWSER_CACHE_MAX_SECONDS = 7 days. A chat
511
+ * that asked for a day was re-downloading images the rest of the product would
512
+ * have served from disk.
513
+ *
514
+ * Applies to previews only. A CLICK must open a live url, so the chip refresh
515
+ * stays on an uncached POST mint.
516
+ */
517
+ declare var PREVIEW_BROWSER_CACHE_SECONDS: number;
491
518
  /**
492
519
  * How long a client may keep serving an href it already minted before dropping
493
520
  * back to the placeholder and re-minting.
@@ -502,10 +529,10 @@ declare function createInlineLinkRegex(): RegExp;
502
529
  declare function safeDecodeURIComponent(v: string): string;
503
530
  declare function encodePathSegments(path: string): string;
504
531
  declare function normalizeAttachmentPathCandidate(value: string): string;
505
- declare function extractRemotePathFromAttachmentHref(href: string, serviceId: string): string | null;
532
+ declare function extractRemotePathFromAttachmentHref(href: string, projectId: string): string | null;
506
533
  declare function getExpiredAttachmentVisiblePath(remotePath: string, fallback?: string): string;
507
534
  declare function buildDisplayExpiredAttachmentHref(remotePath: string, fallback?: string): string;
508
- declare function isServiceDbAttachmentHref(href: string, serviceId: string): boolean;
535
+ declare function isServiceDbAttachmentHref(href: string, projectId: string): boolean;
509
536
  /**
510
537
  * Read the storage path back out of an `_expired_.url` placeholder.
511
538
  *
@@ -515,7 +542,7 @@ declare function isServiceDbAttachmentHref(href: string, serviceId: string): boo
515
542
  * way back in. Returns null for anything that is not the carrier.
516
543
  */
517
544
  declare function readExpiredAttachmentHref(href: string): string | null;
518
- declare function sanitizeAttachmentLinksForHistory(content: string, serviceId: string, forAssistant?: boolean): string;
545
+ declare function sanitizeAttachmentLinksForHistory(content: string, projectId: string, forAssistant?: boolean): string;
519
546
  /**
520
547
  * Is this markdown link target a URL rather than a db storage path?
521
548
  *
@@ -565,6 +592,33 @@ declare function repairUrlEntities(href: string): string;
565
592
  * `src::a/b.pdf).` -> `src::a/b.pdf`, while a balanced `file (v2).pdf` is kept.
566
593
  */
567
594
  declare function normalizeTrailingInlineToken(value: string): string;
595
+ /**
596
+ * Extensions a BROWSER can paint in an <img>, mapped to the content type the
597
+ * presign must declare.
598
+ *
599
+ * The content type is not optional here. get_signed_url only sets
600
+ * ResponseContentType when the caller passes `contentType`, and otherwise falls
601
+ * back to application/octet-stream, which a new tab DOWNLOADS instead of
602
+ * displaying. Since the whole point of the preview is that clicking it shows the
603
+ * picture, the mint has to name the real type.
604
+ *
605
+ * Deliberately narrower than the extraction/vision lists elsewhere in the repo:
606
+ * heic/heif out: Safari paints them, Chrome and Firefox show a broken image,
607
+ * and it is the format every iPhone photo arrives in, so the
608
+ * failure would be common and would read as a bug.
609
+ * tif/wmf/emf out: no mainstream browser paints them.
610
+ * svg out: inside an <img> an SVG is script-disabled and safe, but this
611
+ * feature's click target is a TOP-LEVEL navigation, where an
612
+ * SVG executes its own <script> in the serving origin with that
613
+ * origin's cookies, from user-uploaded content. A preview is an
614
+ * invitation to click exactly that.
615
+ */
616
+ declare var PREVIEWABLE_IMAGE_CONTENT_TYPES: Record<string, string>;
617
+ /** Extension of a path or url, query and fragment stripped, '' when none. */
618
+ declare function previewableExtOf(nameOrPath: string | null | undefined): string;
619
+ declare function isPreviewableImagePath(nameOrPath: string | null | undefined): boolean;
620
+ /** Content type to hand the presign so a new tab displays rather than downloads. */
621
+ declare function previewImageContentType(nameOrPath: string | null | undefined): string | null;
568
622
  /** A link the view renders. `expired` means the href is the `_expired_.url`
569
623
  * placeholder and a click must mint a fresh one from `remotePath`. */
570
624
  interface InlineLinkPart {
@@ -575,10 +629,19 @@ interface InlineLinkPart {
575
629
  expired: boolean;
576
630
  expiredHref?: string;
577
631
  remotePath?: string;
632
+ /**
633
+ * Set only for a file WE host whose PATH says a browser can paint it. Its
634
+ * presence IS the "render a preview" decision, so a view never re-tests the
635
+ * label and never tests `href` (which is the _expired_.url placeholder).
636
+ */
637
+ image?: {
638
+ ext: string;
639
+ contentType: string;
640
+ };
578
641
  }
579
642
  interface InlineLinkContext {
580
643
  /** Current project id: the leading segment to strip off a db url. */
581
- serviceId: string;
644
+ projectId: string;
582
645
  /** `https://db.<hostDomain>` for this deployment. */
583
646
  dbHostPrefix: string;
584
647
  /** A fresh url already minted for this placeholder, if the view cached one. */
@@ -601,8 +664,182 @@ declare function classifyInlineLink(full: string, groups: Array<string | undefin
601
664
  part: InlineLinkPart;
602
665
  tail?: string;
603
666
  } | null;
667
+ /**
668
+ * "We asked for a url for this file and did not get one."
669
+ *
670
+ * A chip the client cannot mint a url for is not a link: the ↗ is a promise it
671
+ * already knows it cannot keep, and clicking it opens a dead tab or nothing at
672
+ * all. Both views therefore keep a map of failures and render those chips
673
+ * unavailable (renderInlineLinkHtml's `unavailable` option): greyed, ✕ instead
674
+ * of ↗, no href.
675
+ *
676
+ * The MAP lives in the view (agent.vue has to re-render when it changes, and
677
+ * that means a ref), so only the keys are here. A failure is reported with
678
+ * exactly one identifier (an image preview knows the storage path, a click knows
679
+ * the placeholder href), so marking writes one key and the lookup tries all of
680
+ * them.
681
+ */
682
+ declare function linkUnavailableKeyForPath(remotePath: string): string;
683
+ declare function linkUnavailableKeyForHref(href: string): string;
684
+ declare function isLinkUnavailable(link: {
685
+ href?: string;
686
+ expiredHref?: string;
687
+ remotePath?: string;
688
+ } | null | undefined, map: Record<string, boolean | undefined> | null | undefined): boolean;
604
689
  declare function truncateLabelForDisplay(label: string): string;
605
690
 
691
+ /**
692
+ * The chip / preview markup for one classified inline link.
693
+ *
694
+ * `classifyInlineLink` was consolidated into the engine because deciding what a
695
+ * link IS had drifted between the two clients and every link bug had to be found
696
+ * and fixed twice. The EMITTER stayed forked, byte for byte identical in
697
+ * agent.vue and the widget. The image preview is the first behaviour that would
698
+ * have had to be written twice, so the emitter moves here too.
699
+ *
700
+ * Pure string in, pure string out: no DOM, no globals, nothing reactive. That is
701
+ * what lets agent.vue keep memoizing parseMsgParts on the message text alone.
702
+ */
703
+ /** Neither client sanitizes bubble HTML (no DOMPurify, no marked sanitize), so
704
+ * everything interpolated here is escaped at the point of interpolation. */
705
+ declare function escapeInlineHtml(v: string | null | undefined): string;
706
+ /**
707
+ * Previews per MESSAGE. Each one costs a presign call and an image download the
708
+ * moment it is hydrated, and a reply listing a folder can name dozens. Past this
709
+ * many the link renders as the ordinary text chip.
710
+ */
711
+ declare var IMAGE_PREVIEWS_PER_MESSAGE: number;
712
+ /**
713
+ * The glyph IS the promise: ↗ says "click this and the file opens". When the
714
+ * client could not get a url for the file, keeping that glyph on a chip it knows
715
+ * is dead is the bug: the click either opens a tab on a 403/404 or, once the
716
+ * href is gone, does nothing at all with no explanation. ✕ says what happened.
717
+ */
718
+ declare var INLINE_LINK_GLYPH: string;
719
+ declare var INLINE_LINK_UNAVAILABLE_GLYPH: string;
720
+ declare var INLINE_LINK_UNAVAILABLE_SUFFIX: string;
721
+ /** Widened so each client's local link-part type is assignable. */
722
+ interface RenderableInlineLink {
723
+ label: string;
724
+ fullLabel?: string;
725
+ href: string;
726
+ expired: boolean;
727
+ expiredHref?: string;
728
+ remotePath?: string;
729
+ image?: {
730
+ ext: string;
731
+ contentType: string;
732
+ };
733
+ }
734
+ interface InlineLinkMarkupOptions {
735
+ /** The view's own "a mint is in flight for this href" flag. */
736
+ refreshing?: boolean;
737
+ /** False once the caller has spent its per-message preview budget. */
738
+ allowImagePreview?: boolean;
739
+ /**
740
+ * The view already tried to get a url for this file and failed (see
741
+ * isLinkUnavailable). Renders a dead chip: ✕, greyed, no href.
742
+ */
743
+ unavailable?: boolean;
744
+ }
745
+ declare function renderInlineLinkHtml(link: RenderableInlineLink, opts?: InlineLinkMarkupOptions): string;
746
+
747
+ /**
748
+ * Give a rendered preview <img> a real src.
749
+ *
750
+ * Why this is not part of the parse: every stored file classifies as
751
+ * `expired: true` with the _expired_.url placeholder as its href, so the url an
752
+ * <img> needs does not exist until something mints it. agent.vue memoizes its
753
+ * parse on the raw message text and drops that cache whenever its link maps
754
+ * change, so minting THROUGH those maps would clear the whole cache once per
755
+ * image and re-run marked over the entire conversation each time. The mint
756
+ * therefore lives here, in a cache that is not reactive and is never read by the
757
+ * parse, and the src is written straight onto the element after render.
758
+ *
759
+ * DOM-free like the rest of the engine: the element type is structural, so a
760
+ * real HTMLImageElement satisfies it while this file imports nothing from
761
+ * lib.dom and touches no global.
762
+ */
763
+ interface PreviewImageEl {
764
+ getAttribute(name: string): string | null;
765
+ setAttribute(name: string, value: string): void;
766
+ removeAttribute(name: string): void;
767
+ addEventListener(type: string, cb: () => void): void;
768
+ }
769
+ interface ImagePreviewContext {
770
+ /** Project id. Namespaces the cache, see clearImagePreviewCache. */
771
+ scope: string;
772
+ /**
773
+ * Mint a directly loadable url for a storage path.
774
+ *
775
+ * `contentType` is not advisory. get_signed_url only sets
776
+ * ResponseContentType when the caller passes one and otherwise falls back to
777
+ * application/octet-stream, which a new tab DOWNLOADS instead of displaying.
778
+ * An implementation that drops this argument still paints the preview (an
779
+ * <img> sniffs the bytes) but silently breaks the click.
780
+ *
781
+ * `refresh` asks for a url the browser cache cannot answer. The mint request
782
+ * itself is cacheable (that is what stops a reload re-downloading every
783
+ * image), so a plain re-mint can hand back the very url that just failed;
784
+ * refresh is how the caller escapes its own cache.
785
+ */
786
+ mint: (remotePath: string, contentType: string, refresh?: boolean) => Promise<string>;
787
+ /** An image finished painting. Views use it to re-pin the scroll. */
788
+ onLoad?: (remotePath: string) => void;
789
+ /** A preview gave up. The caption chip is now the whole answer. */
790
+ onError?: (remotePath: string, err: unknown) => void;
791
+ }
792
+ /**
793
+ * Drop cached preview urls, for one project or all of them.
794
+ *
795
+ * The key carries the project because an identity-blind cache is how one
796
+ * project's content has reached another project's chat before. Call on project
797
+ * switch and on sign-out.
798
+ */
799
+ declare function clearImagePreviewCache(scope?: string): void;
800
+ /**
801
+ * A url we already hold that is still comfortably alive, or null.
802
+ *
803
+ * Synchronous on purpose. Both views rebuild bubble DOM constantly (the widget
804
+ * tears down and re-creates every node in renderMessages; Vue re-patches v-html
805
+ * whenever the string changes), so an async-only assignment would blank every
806
+ * visible image for a frame on each re-render. Called before paint, this makes a
807
+ * re-render invisible.
808
+ */
809
+ declare function peekImagePreviewUrl(ctx: ImagePreviewContext, remotePath: string): string | null;
810
+ /**
811
+ * Deduped, TTL-bounded mint for one path.
812
+ *
813
+ * `refresh` skips every cache in the way: this module's in-memory one, and the
814
+ * browser's cache of the mint request itself. It is what the error path uses to
815
+ * replace a url that has expired, and what a view would call to pick up a file
816
+ * that changed.
817
+ */
818
+ declare function resolveImagePreviewUrl(ctx: ImagePreviewContext, remotePath: string, contentType: string, refresh?: boolean): Promise<string>;
819
+ /**
820
+ * Declare a stored file changed, so the next mint for it goes to the network.
821
+ *
822
+ * The mint request is browser-cached, which is what stops a reload re-downloading
823
+ * every image, but it also means an OVERWRITE is invisible: the file is re-posted
824
+ * to the same storage path, so the mint url is byte-identical, so the browser
825
+ * replays the cached mint and hands back the same signed url and the same cached
826
+ * body. The preview would keep painting the previous picture for a day.
827
+ *
828
+ * Upload paths call this because they are the one place that KNOWS the bytes
829
+ * changed. It is a marker, not a fetch: the refresh happens on the next mint,
830
+ * which is when the new bubble actually renders. In memory only, deliberately.
831
+ */
832
+ declare function markImagePreviewStale(scope: string, remotePath: string): void;
833
+ /**
834
+ * Hydrate every un-claimed preview <img> in the given list.
835
+ *
836
+ * Idempotent: an element is claimed by its own data-bq-img-state, so a full
837
+ * re-render, a Vue patch, or two calls in one frame cannot mint twice. The
838
+ * caller decides WHICH elements to pass, so a view can move from
839
+ * querySelectorAll to an IntersectionObserver without an engine change.
840
+ */
841
+ declare function hydrateImagePreviews(imgs: ArrayLike<PreviewImageEl>, ctx: ImagePreviewContext): void;
842
+
606
843
  /**
607
844
  * Chat timestamp formatting, shared so agent.vue and the widget render an
608
845
  * identical "small text under the bubble". Pure and locale-aware: it formats a
@@ -677,7 +914,7 @@ type IndexingRequestRef = {
677
914
  declare function parseIndexingRequestText(userText: any): IndexingRequestRef | null;
678
915
  type MapHistoryOptions = {
679
916
  clearedAt: number;
680
- serviceId: string;
917
+ projectId: string;
681
918
  /** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
682
919
  formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean) => string;
683
920
  };
@@ -772,7 +1009,29 @@ declare function fillHistoryViewport(opts: FillHistoryViewportOptions): Promise<
772
1009
  * the loop then keeps paging until EVERY caller is satisfied. Predicates that
773
1010
  * come true are dropped as it goes, so the cost stays flat.
774
1011
  */
775
- declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isSatisfied'>): {
1012
+ declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isSatisfied'> & {
1013
+ /** Fired when the loop starts FETCHING and when it stops, and only on a real
1014
+ * change.
1015
+ *
1016
+ * This — not the caller's own per-request `isLoading` — is what "older
1017
+ * history is still coming in" means to a view. A fill is many pages, and
1018
+ * `isLoading` drops to false between every one of them, so anything
1019
+ * rendered off it flickers once per page for the whole loop. A collapsed
1020
+ * indexing row whose run begins above the loaded window renders exactly
1021
+ * that ("still loading this run" vs a status it cannot know yet), which is
1022
+ * why the loop has to publish its own span.
1023
+ *
1024
+ * Fetching, NOT requested. Most fills fetch nothing: they are fired on every
1025
+ * window resize, every row a user collapses, and every first-page load, and
1026
+ * the overwhelmingly common outcome is `isSatisfied` returning true on the
1027
+ * first look. Announcing at request time published a true/false pair for
1028
+ * each of those, and the widget's own satisfied-check spans two animation
1029
+ * frames — long enough for the browser to PAINT the intermediate state. Every
1030
+ * collapsed row strobed through "loading" on every resize tick. So the span
1031
+ * opens at the first actual page request, which is also the first moment the
1032
+ * claim is true. */
1033
+ onRunningChange?: (running: boolean) => void;
1034
+ }): {
776
1035
  fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>;
777
1036
  isRunning: () => boolean;
778
1037
  };
@@ -780,6 +1039,33 @@ declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isS
780
1039
  declare const MCP_NAME = "BunnyQuery";
781
1040
  declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
782
1041
  declare const DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
1042
+ /** How a given model should be shown a rendered document. */
1043
+ type VisionProfile = {
1044
+ /** Per-image `detail` (OpenAI only). */
1045
+ detail: string;
1046
+ /** Pages the worker renders into one window. */
1047
+ pagesPerWindow: number;
1048
+ /** Horizontal bands per page; 1 renders the whole page as one image. */
1049
+ tile: number;
1050
+ };
1051
+ /**
1052
+ * Resolve the render profile for a model.
1053
+ *
1054
+ * Three tiers, and they fail for different reasons, which is why one set of knobs cannot
1055
+ * serve all of them:
1056
+ * - full: everything ABOVE gpt-5.4-nano - the base models, mini, and every gpt-5.6
1057
+ * including 5.6-nano. These already transcribe dense scans correctly, so they get
1058
+ * 'original' detail and are otherwise untouched.
1059
+ * - gpt-5.4-nano: also gets 'original', so it sees exactly the SAME pixels as the full
1060
+ * tier. Its gap therefore is not resolution, and tiling would do nothing for it. What
1061
+ * it lacks is room: a smaller window leaves the same output budget divided between
1062
+ * fewer pages.
1063
+ * - downsampled (below the 'original' floor: gpt-5.3-nano, gpt-5-nano, gpt-4.1-nano):
1064
+ * capped at 'high', which resamples the page onto a 512px grid no matter what DPI it
1065
+ * was rendered at. Render resolution is wasted on these entirely; the only way to give
1066
+ * them readable text is to make each image cover less of the page, which is `tile`.
1067
+ */
1068
+ declare function getVisionProfile(model?: string): VisionProfile;
783
1069
  type ClaudeRole = 'user' | 'assistant';
784
1070
  type ClaudeMessage = {
785
1071
  role: ClaudeRole;
@@ -818,6 +1104,7 @@ type CallClaudeWithMcpParams = {
818
1104
  onError?: (err: any) => void;
819
1105
  };
820
1106
  declare const POLL_INTERVAL = 3000;
1107
+ declare const MAX_CONCURRENT_BG_POLLS = 6;
821
1108
  declare function callClaudeWithMcp({ prompt, messages, service, owner, userId, model, maxTokens, system, mcpServer, extractContent, fileUrls, }: CallClaudeWithMcpParams): Promise<any>;
822
1109
  declare function callClaudeWithPublicMcp(prompt: string, service: string, owner: string, messages?: ClaudeMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
823
1110
  declare function callOpenAIWithPublicMcp(prompt: string, service: string, owner: string, messages?: OpenAIMessage[], system?: string, model?: string, userId?: string, extractContent?: ExtractDirective[], fileUrls?: FileUrlDirective[], onResponse?: (res: any) => void, onError?: (err: any) => void): Promise<any>;
@@ -825,8 +1112,20 @@ type AttachmentSaveInfo = {
825
1112
  platform: 'claude' | 'openai';
826
1113
  model?: string;
827
1114
  service: string;
1115
+ /** The PUBLIC project ID (formatted token, skapi.project_id). Shown to the model; falls back to `service`. */
1116
+ publicProjectId?: string;
828
1117
  owner: string;
829
- userId?: string;
1118
+ /**
1119
+ * Queue base for this indexing pass: "<userId>-bg". REQUIRED, and it must be
1120
+ * the SAME value the chat turn uses (ChatSession.dispatchComposedMessage's
1121
+ * `id.userId || id.projectId`) — the backend serialises requests that share a
1122
+ * queue name and runs different ones IN PARALLEL, so a pass enqueued under a
1123
+ * different base does not hold the chat back at all. It was optional once,
1124
+ * defaulting to `service`; the chatbox omitted it, and its files were indexed
1125
+ * on "<projectId>-bg" while its question ran on "<userId>-bg" — the question
1126
+ * was answered from a file nothing had read yet. Pass `userId || projectId`.
1127
+ */
1128
+ userId: string;
830
1129
  serviceName?: string;
831
1130
  serviceDescription?: string;
832
1131
  attachment: {
@@ -861,6 +1160,14 @@ declare function extractOpenAIText(response: any): any;
861
1160
  declare function listClaudeModels(service: string, owner: string): Promise<any>;
862
1161
  declare function listOpenAIModels(service: string, owner: string): Promise<any>;
863
1162
  declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
1163
+ /**
1164
+ * The one place the background-indexing queue name is spelled out. The backend
1165
+ * serialises requests sharing a queue name and runs different names in PARALLEL,
1166
+ * so every indexing pass AND the chat turn that must wait behind them have to
1167
+ * resolve to the identical string — see AttachmentSaveInfo.userId for what
1168
+ * happens when they do not.
1169
+ */
1170
+ declare function bgIndexingQueueName(userId?: string, service?: string): string;
864
1171
  /**
865
1172
  * True when a request belongs to the background-indexing queue.
866
1173
  *
@@ -872,7 +1179,7 @@ declare const BG_INDEXING_QUEUE_SUFFIX = "-bg";
872
1179
  */
873
1180
  declare function isBgIndexingQueue(queueName?: string): boolean;
874
1181
  type BgTaskEntry = {
875
- serviceId: string;
1182
+ projectId: string;
876
1183
  platform: 'claude' | 'openai';
877
1184
  id: string;
878
1185
  filename: string;
@@ -886,7 +1193,16 @@ type BgTaskEntry = {
886
1193
  }) => Promise<any>) | undefined;
887
1194
  /** How many CONTINUE passes have already run for this file (resume-across-passes). */
888
1195
  resumePass?: number;
1196
+ /** The STAGED chat turn these files were attached to (ChatSession.stageOutgoingMessage).
1197
+ * drainBgTaskQueue inserts this pass's bubble directly ABOVE that turn's bubble, so the
1198
+ * collapsed row sits where the reader expects it — right before the message the files
1199
+ * came with — from the moment it appears, instead of the turn being moved down past it
1200
+ * once everything finishes. Absent for work with no chat turn behind it (the dbfile
1201
+ * page, an attachment-only send, a worker-adopted pass), which appends as before. */
1202
+ stageId?: string;
889
1203
  };
1204
+ declare const INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
1205
+ declare const EMPTY_INDEXING_REPLY = "Finished reading this file.";
890
1206
  /**
891
1207
  * `queue` narrows the fetch to one processing chain; `status` narrows it to items
892
1208
  * in one state. Passing both is how the client asks "is there still unresolved
@@ -912,9 +1228,17 @@ declare function getChatHistory(params: {
912
1228
  * otherwise touch the DOM or a framework goes through a host hook.
913
1229
  */
914
1230
  interface ChatIdentity {
915
- serviceId: string;
1231
+ projectId: string;
1232
+ /**
1233
+ * The PUBLIC project ID: the formatted two-segment token (skapi.project_id).
1234
+ * projectId above is the RAW regional code the wire endpoints take; the public
1235
+ * token is what MCP tools accept and what prompts must show the model, since the
1236
+ * model copies it verbatim into tool calls. Optional for older hosts; prompts
1237
+ * fall back to the raw code when absent.
1238
+ */
1239
+ publicProjectId?: string;
916
1240
  owner: string;
917
- /** Per-user queue name (falls back to serviceId). */
1241
+ /** Per-user queue name (falls back to projectId). */
918
1242
  userId: string;
919
1243
  platform: 'claude' | 'openai' | 'none';
920
1244
  model?: string;
@@ -931,6 +1255,11 @@ interface ChatIdentity {
931
1255
  interface PinnedDispatchContext {
932
1256
  identity: ChatIdentity;
933
1257
  systemPrompt: string;
1258
+ /** Id returned by stageOutgoingMessage. The turn's bubble is already on
1259
+ * screen (staged while its attachments upload), so dispatchComposedMessage
1260
+ * REPLACES that bubble in place instead of pushing a second one at the
1261
+ * bottom — the message keeps the position it was sent in. */
1262
+ stageId?: string;
934
1263
  }
935
1264
  /**
936
1265
  * The file a background-indexing bubble belongs to. Stamped on the REQUEST
@@ -957,13 +1286,50 @@ interface ChatMessage {
957
1286
  isPendingInProcess?: boolean;
958
1287
  isPendingQueued?: boolean;
959
1288
  isPendingOlder?: boolean;
1289
+ /** PROTOCOL flag: true from the moment a queued turn is dispatched until the
1290
+ * server acknowledges it. It is the token the ack's findIndex matches on, so
1291
+ * nothing may clear it early. It is NOT a style input — see _dimSending. */
960
1292
  isSendingToServer?: boolean;
1293
+ /** PRESENTATIONAL flag: render this bubble dimmed because the turn has not been
1294
+ * handed over yet. Split from isSendingToServer because an ATTACHMENT turn is
1295
+ * un-dimmed the instant its files finish indexing, while the request itself is
1296
+ * still un-acked for another moment; dropping isSendingToServer to achieve that
1297
+ * would cost the turn its _serverItemId (the ack matches on that flag alone, and
1298
+ * a _useBgQueue turn is excluded from every fallback that would recover it). */
1299
+ _dimSending?: boolean;
961
1300
  isCancelled?: boolean;
962
1301
  isError?: boolean;
963
1302
  isBackgroundTask?: boolean;
964
1303
  /** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
965
1304
  _indexFile?: IndexingFileRef;
1305
+ /** Set on a background-indexing RESPONSE bubble whose raw answer carried the
1306
+ * INDEXING_COMPLETE marker. Stamped before the marker is stripped for display,
1307
+ * in every path that builds one (live resolution and both history mappers), so
1308
+ * a run reads the same before and after a reload.
1309
+ *
1310
+ * Meaningful ONLY for a client-driven chain, where it is the very signal
1311
+ * maybeResumeIndexing stops on. The worker-driven paths (PDF vision, windowed
1312
+ * reads) advance off the renderer's page count and their prompt deliberately
1313
+ * never asks for the marker, so a model that emits one there is guessing —
1314
+ * which is how an 88-page file once "finished" at page 15. */
1315
+ _indexComplete?: boolean;
966
1316
  _useBgQueue?: boolean;
1317
+ /** Local id of a turn STAGED at Send time while its attachments upload. The
1318
+ * bubble exists before any server request does, so it is never matched by
1319
+ * _serverItemId and is never promoted/cancelled by the queue machinery —
1320
+ * dispatchComposedMessage consumes it (pinned.stageId) when the turn is
1321
+ * finally sent. Staged bubbles are deliberately kept OUT of the history
1322
+ * cache: an unmount kills the upload that would resolve them, so a cached
1323
+ * copy would replay as a bubble that uploads forever. */
1324
+ _stageId?: string;
1325
+ /** Staged-turn phase 1: its files are still uploading. Renders
1326
+ * "(Uploading files...)", dimmed. */
1327
+ isUploadingAttachments?: boolean;
1328
+ /** Staged-turn phase 2: the files are up and the turn is waiting for the whole
1329
+ * background-indexing chain behind them to finish. Renders "(Indexing files...)",
1330
+ * still dimmed. Cleared (with _dimSending) by markStagedMessageReady the moment
1331
+ * the queue drains, which is when the turn genuinely becomes "(In queue)". */
1332
+ isAwaitingIndexing?: boolean;
967
1333
  _serverItemId?: string;
968
1334
  _localId?: string;
969
1335
  _cancelling?: boolean;
@@ -991,6 +1357,28 @@ interface ChatState {
991
1357
  historyStartKeyHistory: string[];
992
1358
  historyRequestToken: number;
993
1359
  gateRefreshToken: number;
1360
+ /** Files the SERVER still has unresolved indexing work for, by the key a
1361
+ * collapsed row uses (storage path, else filename). Lives on the state rather
1362
+ * than privately so a reactive consumer re-renders when it changes. */
1363
+ liveIndexKeys: {
1364
+ [fileKey: string]: boolean;
1365
+ };
1366
+ /** Whether `liveIndexKeys` has been answered at least once for this chat. False
1367
+ * means "we have not found out", which the display layer reads as still
1368
+ * working — never as an all-clear. */
1369
+ liveIndexChecked: boolean;
1370
+ /** Server item ids of the indexing passes that existed — on the row, or on the
1371
+ * bg queue — when the user STOPPED that file. Two readers, one fact:
1372
+ * buildChatDisplayList reports the run as stopped when it holds any of them
1373
+ * (a stop routinely leaves no other trace), and _applyIndexCancellations
1374
+ * refuses to let one of them lift the stop the way a genuinely new indexing
1375
+ * request does. Ids, not file keys: they name the RUN that was stopped, so a
1376
+ * later re-index of the same file cannot inherit it. On the state, like
1377
+ * liveIndexKeys, so a reactive consumer re-renders the moment a stop is
1378
+ * recorded — a stop with nothing left to cancel changes no message at all. */
1379
+ stoppedIndexIds: {
1380
+ [serverItemId: string]: boolean;
1381
+ };
994
1382
  }
995
1383
  interface ChatHost {
996
1384
  /** Read live (platform/model/name can change between sends). */
@@ -1044,6 +1432,16 @@ interface ChatHost {
1044
1432
  * through to a plain re-index. Implementations must be best-effort (swallow
1045
1433
  * "not found" / permission errors so indexing still proceeds). */
1046
1434
  deleteExistingFileRecord?(storagePath: string): Promise<any>;
1435
+ /**
1436
+ * Create the file's "src::<storagePath>" record before indexing starts, so every pass has a
1437
+ * reference target that exists. Optional: a host without it keeps the old behaviour, where
1438
+ * whichever pass got there first created the record and the others hoped it had.
1439
+ */
1440
+ ensureFileIndexRecord?(storagePath: string, meta?: {
1441
+ name?: string;
1442
+ mime?: string;
1443
+ size?: number;
1444
+ }): Promise<any>;
1047
1445
  /** Map a relative path to the consumer's db storage key (e.g. uid-prefixed). */
1048
1446
  storagePathFor(relPath: string): string;
1049
1447
  getMimeType(name: string): string | null;
@@ -1101,6 +1499,13 @@ interface ChatHost {
1101
1499
  * failed), how many passes are currently loaded, and `mayHaveOlder` when the
1102
1500
  * file's first pass is not among them.
1103
1501
  *
1502
+ * For the same reason it also reports NOT KNOWING. A run whose start is still
1503
+ * being paged in, and a worker-driven run whose queue state has not been asked
1504
+ * for yet, are both rows whose state is a moving target — and on a chatbox that
1505
+ * was just opened, that is most of them. `resolving` marks those, so the view can
1506
+ * say which wait it is waiting on rather than committing to "indexing" or
1507
+ * "indexed" on a fraction of the evidence.
1508
+ *
1104
1509
  * Pure and view-agnostic: agent.vue and the BunnyQuery widget both render from
1105
1510
  * this, so the two stay identical.
1106
1511
  */
@@ -1110,7 +1515,16 @@ type IndexingGroup = {
1110
1515
  /** The FILE this row is about: storage path when known (a file can be
1111
1516
  * re-uploaded under a name that already exists elsewhere), else name. Shared
1112
1517
  * by every run of that file, and what ChatSession.cancelIndexingGroup and
1113
- * _indexKeyOf match on — never use it as a render key. */
1518
+ * _indexKeyOf match on — never use it as a render key.
1519
+ *
1520
+ * It IS the key for persistent view state, above all the expansion state. That
1521
+ * used to be keyed by runKey, which is renamed the moment a run's true first
1522
+ * pass loads (see below) — so a row the user had opened silently closed itself
1523
+ * mid-indexing, every time a pass arrived ahead of the earlier ones. This never
1524
+ * changes for the life of a file. The cost is that two runs OF THE SAME FILE
1525
+ * (an index and a later re-index) open and close together, which is a fair
1526
+ * reading of "show me this file's steps" and is not a state the user can be
1527
+ * surprised out of. */
1114
1528
  key: string;
1115
1529
  /** Identity of this ROW: one indexing RUN of that file. A file indexed on
1116
1530
  * Monday and re-indexed on Wednesday is two runs, and collapsing them into
@@ -1118,8 +1532,13 @@ type IndexingGroup = {
1118
1532
  * its passes for Wednesday, and let Monday's failure be overwritten by
1119
1533
  * Wednesday's success. Named after the run's FIRST loaded pass (see where it
1120
1534
  * is assigned below), so passes appended to the run and other runs appearing
1121
- * on either side of it never rename a row already on screen. This is the
1122
- * render key and the expansion key. */
1535
+ * on either side of it never rename a row already on screen.
1536
+ *
1537
+ * This is the RENDER key, and only that. It is renamed when the run's true
1538
+ * first pass finally loads — routine while a worker-driven chain is running,
1539
+ * since a pass adopted from the queue can reach the client before the earlier
1540
+ * ones are paged in — and a rename is exactly right for a DOM key (the row did
1541
+ * change identity) but wrong for anything the USER set. Key that on `key`. */
1123
1542
  runKey: string;
1124
1543
  name: string;
1125
1544
  path?: string;
@@ -1141,8 +1560,19 @@ type IndexingGroup = {
1141
1560
  * when nothing is cancellable — a finished file, or a live pass whose server
1142
1561
  * id has not come back yet. */
1143
1562
  cancellableIds: string[];
1144
- /** A cancel request is in flight for one of the passes. */
1563
+ /** This row is in the middle of stopping: a cancel request is in flight for one
1564
+ * of its passes, or the user stopped the run and a pass is still running. Both
1565
+ * mean the same thing to a view — the Stop has been spent, so the button reads
1566
+ * "Stopping..." and is not offered again. */
1145
1567
  cancelling: boolean;
1568
+ /** The user stopped this run.
1569
+ *
1570
+ * NOT the same as `status === 'cancelled'`, and the difference is the whole
1571
+ * reason it exists: a stop landing on a pass that is already RUNNING cannot
1572
+ * un-run it, so the row stays `active` until that pass settles. `status` then
1573
+ * describes the work (something is still running) and this describes the user's
1574
+ * decision (no more of it will be started). */
1575
+ stopped: boolean;
1146
1576
  /** Why the last cancel attempt failed (e.g. the pass had already finished). */
1147
1577
  cancelError?: string;
1148
1578
  /** The file's first pass is not among the loaded messages, so earlier passes
@@ -1158,6 +1588,74 @@ type IndexingGroup = {
1158
1588
  * must not re-derive it from `members`: which member the row renders at is
1159
1589
  * this module's decision, and the two silently disagreed once already. */
1160
1590
  anchorId: string;
1591
+ /** `members` minus the turns an EXPANDED row should not show: every CONTINUE
1592
+ * request, and the running pass's empty placeholder.
1593
+ *
1594
+ * A continuation's request bubble says "Indexing (continuing) <file>" and
1595
+ * nothing else — it repeats the row's own header once per pass, so a long file
1596
+ * read as the same line over and over with the actual findings buried between
1597
+ * them. The pass is still represented, by its RESPONSE. The placeholder goes
1598
+ * for a different reason: the row now carries one loader of its own for as long
1599
+ * as work remains, and two spinners in one open row is noise.
1600
+ *
1601
+ * Additive. `members` is untouched and is still what every count, status,
1602
+ * cancel and anchor decision reads — several of them are only correct on the
1603
+ * full list (a `mayHaveOlder` run's members[0] IS a continuation). */
1604
+ visibleMembers: {
1605
+ msg: ChatMessage;
1606
+ index: number;
1607
+ }[];
1608
+ /** Who advances this file's chain, which decides what can confirm it is over:
1609
+ * 'single' one pass and done; 'client' this client dispatches each CONTINUE
1610
+ * pass and stops on the model's completion marker; 'worker' the server advances
1611
+ * the loop off the renderer's page count and the client is only a spectator. */
1612
+ driver: 'single' | 'client' | 'worker';
1613
+ /** Positively established that no further indexing work will happen for this
1614
+ * run. NOT "the file was fully read" — a cap-out, a failure and a stop are all
1615
+ * finished, and the row's own status says which.
1616
+ *
1617
+ * False means "not established", which includes "still running" AND "we have
1618
+ * not been able to find out". The view shows a loader for both, deliberately:
1619
+ * the alternative default is the failure this exists to prevent, a row that
1620
+ * reads "Indexed" between two passes of a file still being read. */
1621
+ finished: boolean;
1622
+ /** This row cannot honestly claim a state yet, because something it is derived
1623
+ * FROM is still being fetched. Both `status` and `finished` are read off the
1624
+ * passes that happen to be LOADED, and on a freshly opened chatbox that is a
1625
+ * moving target: history pages newest-first, so a long run arrives as a tail
1626
+ * of CONTINUE passes while its beginning is still being paged in. The row was
1627
+ * picking a side through that window — a spinner reading "Indexing" for a file
1628
+ * that finished last week, or a green "Indexed" for one still being read — and
1629
+ * both are verdicts drawn from a fraction of the run.
1630
+ *
1631
+ * Only ever set from status 'done'. A loaded pending pass PROVES the run is
1632
+ * live, and an error or a stop is the newest pass's own outcome, which
1633
+ * newest-first paging always has in hand — none of those is a guess, and
1634
+ * hiding any of them behind a loader would lose something the user needs.
1635
+ *
1636
+ * For the 'history' reason this means "a fetch is IN FLIGHT", not "the picture
1637
+ * is incomplete". Older history is paged in by explicit triggers only (the
1638
+ * viewport fill, the user scrolling to the top) and nothing auto-fetches on a
1639
+ * row's behalf, so a run whose start is still unloaded once the paging stops
1640
+ * has to go back to reporting what it does know — `mayHaveOlder` and the `+` on
1641
+ * the pass count carry the rest. A "loading..." that never ends is the same lie
1642
+ * pointing the other way.
1643
+ *
1644
+ * The 'status' reason is weaker on purpose: "the queue has not answered", which
1645
+ * a permanently failing query never resolves. That is deliberate, because it is
1646
+ * the SAME question as what the row should say when it cannot find out, and
1647
+ * every alternative is worse: a grey clock reading "checking" claims less than
1648
+ * the yellow spinner reading "Indexing" that it replaced. It also self-heals in
1649
+ * practice — the answer is re-sought on every first-page history load and every
1650
+ * settling pass — and gating it on an in-flight query instead would mean
1651
+ * threading a second liveness flag through a retry ladder with nine exit
1652
+ * points, i.e. trading this for a flag that can stick in the other direction. */
1653
+ resolving: boolean;
1654
+ /** Which wait, so the row can name it instead of just spinning. 'history':
1655
+ * older pages are being fetched and this run's first pass is not among the
1656
+ * loaded ones. 'status': the queue has not yet said whether this file is still
1657
+ * being worked on, which is the only thing that can end a worker-driven run. */
1658
+ resolvingReason?: 'history' | 'status';
1161
1659
  };
1162
1660
  type DisplayEntry = {
1163
1661
  kind: 'message';
@@ -1172,6 +1670,34 @@ type BuildDisplayListOptions = {
1172
1670
  /** True while older history remains unpaged, which is what makes a group
1173
1671
  * with no first pass genuinely incomplete rather than merely odd. */
1174
1672
  hasMoreHistory?: boolean;
1673
+ /** An OLDER-history fetch is in flight right now — a single page, or the whole
1674
+ * viewport-fill loop (createHistoryFiller's onRunningChange, which spans the
1675
+ * pages between which a per-request flag keeps dropping to false).
1676
+ *
1677
+ * Older specifically. A first-page refresh cannot bring in a run's earlier
1678
+ * passes, so counting it here would flip every incomplete row to "still
1679
+ * loading" for the length of a poll that could never have answered it. */
1680
+ loadingOlderHistory?: boolean;
1681
+ /** Files the SERVER still has unresolved indexing work for, keyed exactly like
1682
+ * IndexingGroup.key (ChatSession.getLiveIndexState). */
1683
+ liveIndexKeys?: {
1684
+ [fileKey: string]: boolean;
1685
+ };
1686
+ /** Whether `liveIndexKeys` has been answered at least once for this chat. False
1687
+ * is "we do not know", and a worker-driven run stays unfinished on it. */
1688
+ liveIndexChecked?: boolean;
1689
+ /** Server item ids of passes that were on a row when the user STOPPED it
1690
+ * (ChatSession.state.stoppedIndexIds). A run holding any of them is a run the
1691
+ * user stopped — see the status derivation for why a stop usually leaves no
1692
+ * other trace in the messages. Ids rather than a file key on purpose: they name
1693
+ * one RUN, so a later re-index of the same file cannot inherit the stop. */
1694
+ stoppedIndexIds?: {
1695
+ [serverItemId: string]: boolean;
1696
+ };
1697
+ /** Whether the WORKER drives the windowed text/grid loop (chatEngineConfig's
1698
+ * windowedIndexing). Passed in rather than read from config so this stays a
1699
+ * pure function of its inputs and can be exercised for both settings. */
1700
+ windowedIndexing?: boolean;
1175
1701
  };
1176
1702
  declare function parseIndexingLabel(content: string): {
1177
1703
  name: string;
@@ -1238,7 +1764,198 @@ declare class ChatSession {
1238
1764
  private _pauseReasons;
1239
1765
  private _resuming;
1240
1766
  private _lidSeq;
1767
+ private _stageSeq;
1768
+ /** How many attachment-upload batches are running. uploadingAttachments is a
1769
+ * single flag but batches overlap (the composer stays live, so the user can
1770
+ * send a second one while the first uploads), and a nested finish must not
1771
+ * clear the flag out from under the batch still running. */
1772
+ private _uploadBatches;
1773
+ /** Indexing requests whose ack has not come back yet. Until it does the item
1774
+ * is not on the server's queue, so awaitIndexingDrained cannot see it — and
1775
+ * would read the gap between "pass N settled" and "pass N+1 accepted" as the
1776
+ * file being finished. */
1777
+ private _indexDispatchesInFlight;
1778
+ /** Live awaitIndexingDrained waiters, one callback each. A nudge only pulls that
1779
+ * waiter's NEXT look forward; it can never make one conclude anything, so a
1780
+ * wrong nudge costs one pair of requests and the look reports busy. Overlapping
1781
+ * waiters are normal — the composer stays live, so a second send can be
1782
+ * uploading while the first waits. */
1783
+ private _drainNudges;
1784
+ /** Stages whose upload/dispatch chain is still running in THIS page. Lives and
1785
+ * dies with those chains, so it is what tells a staged bubble restored from the
1786
+ * history cache whether anything is still working on it (see
1787
+ * settleDeadStagedMessages). Today the cache dies with the page too and every
1788
+ * restored stage is live; this stays correct if that ever changes. */
1789
+ private _liveStages;
1790
+ /** Files the SERVER currently has unresolved indexing work for, by the same key
1791
+ * a collapsed row uses (storage path, else filename), and whether we have asked
1792
+ * even once for this chat.
1793
+ *
1794
+ * This is the only thing that can tell a WORKER-driven run (a PDF's page loop, a
1795
+ * windowed read) that it is over. Those chains are advanced inside the worker off
1796
+ * the renderer's page count; the client sees passes appear and settle and can
1797
+ * never tell "between passes" from "finished" by looking at them. Asking the
1798
+ * queue is how it finds out. Until it has asked, `checked` is false and the view
1799
+ * says "still working", which is the honest reading of not knowing — and the one
1800
+ * that does not repeat the bug where a row claimed "Indexed" mid-run. */
1801
+ /** The chat the live-index snapshot (state.liveIndexKeys) was taken for, so a
1802
+ * project switch drops it. */
1803
+ private _liveIndexKey;
1804
+ /** When the snapshot was last published (wall clock ms), so a caller that needs
1805
+ * a CURRENT answer can tell whether to re-ask. 0 = never. */
1806
+ private _liveIndexAt;
1807
+ /** Files this client has an index dispatch in flight for, by scoped path ->
1808
+ * wall clock. See claimIndexRun. */
1809
+ private _indexClaims;
1241
1810
  constructor(host: ChatHost);
1811
+ /** What the display layer needs to decide whether a run is finished. `keys` holds
1812
+ * every file the server still has indexing work for; `checked` is false until the
1813
+ * first answer for this chat, and false means "we do not know yet". */
1814
+ getLiveIndexState(): {
1815
+ keys: {
1816
+ [fileKey: string]: boolean;
1817
+ };
1818
+ checked: boolean;
1819
+ };
1820
+ /** Passes that were on a row when the user stopped it, so the display layer can
1821
+ * still tell that this run was stopped once the stop has left no other trace.
1822
+ * See cancelIndexingGroup, which fills it, and buildChatDisplayList, which is
1823
+ * the only reader. */
1824
+ getStoppedIndexIds(): {
1825
+ [serverItemId: string]: boolean;
1826
+ };
1827
+ /**
1828
+ * Is this file ALREADY being indexed by this client?
1829
+ *
1830
+ * One live run per file, and the reason is what a second one looks like: the
1831
+ * conversation grows a SECOND collapsed row for the same file (a run is opened
1832
+ * by every FIRST pass, so two of them are two rows), the same document is read
1833
+ * twice at full provider cost, and the two chains fight over the same records —
1834
+ * the delete-then-repost that starts run 2 wipes what run 1 has saved so far.
1835
+ *
1836
+ * Asked of this client's own live work, so it cannot be wrong in the dangerous
1837
+ * direction: a queued/running pass keeps its bgTaskQueue entry until its bubble
1838
+ * settles, and a settled run answers false, which is what a genuine later
1839
+ * re-index needs.
1840
+ *
1841
+ * The retry that made this necessary: a chip whose INDEX request failed is
1842
+ * handed back to the composer to be retried on the next send, and an index
1843
+ * request can fail from the client's side (a lost ack, an expired token on the
1844
+ * response) while the server has already queued the pass. The retry then indexes
1845
+ * a file that was never not being indexed.
1846
+ */
1847
+ hasLiveIndexRun(storagePath?: string): boolean;
1848
+ /** Storage paths are project-relative, and one ChatSession serves every
1849
+ * project, so a claim has to be scoped the way a stop is (_indexKeyOf). */
1850
+ private _indexClaimKey;
1851
+ /**
1852
+ * Take this file's indexing slot, or report that someone already has it.
1853
+ *
1854
+ * The check-and-CLAIM is what makes it safe against a second caller arriving
1855
+ * mid-flight: the claim is written SYNCHRONOUSLY, before the first await, so a
1856
+ * concurrent caller sees it even though no request has completed and no queue
1857
+ * has admitted anything. Ask-then-dispatch could not do that — every source it
1858
+ * consults only learns about a dispatch after the ack.
1859
+ *
1860
+ * Returns true when the caller owns the slot and should dispatch. A caller that
1861
+ * then fails to dispatch MUST releaseIndexRun, or the file waits out the claim
1862
+ * (a few minutes) before it can be retried.
1863
+ */
1864
+ claimIndexRun(storagePath?: string): Promise<boolean>;
1865
+ /** Give the slot back — the dispatch failed, or was abandoned. */
1866
+ releaseIndexRun(storagePath?: string): void;
1867
+ /**
1868
+ * The same question, asked of the SERVER when this page cannot answer it.
1869
+ *
1870
+ * hasLiveIndexRun only knows what this page did. That is not enough for the
1871
+ * case duplicates actually come from: the first run was started before a
1872
+ * reload, or in another tab, or its bubble has since been paged out of the
1873
+ * loaded window — and then the retry finds nothing locally and starts a second
1874
+ * run of a file that is still being indexed. The queue is the one place that
1875
+ * knows, and it is already asked for exactly this list.
1876
+ *
1877
+ * Only a POSITIVE answer is used. Absence proves nothing here (the query is
1878
+ * capped, and `liveIndexChecked` records that), so an unanswerable question
1879
+ * falls back to dispatching — the cost of a wrong "no" is the duplicate this
1880
+ * exists to prevent, and the cost of a wrong "yes" is a file that never gets
1881
+ * indexed at all. Only one of those is recoverable by the user.
1882
+ */
1883
+ isIndexRunLive(storagePath?: string): Promise<boolean>;
1884
+ /** Re-ask the queue which files are still being indexed, unless the answer we
1885
+ * have is younger than `maxAgeMs`. Shared by every caller that needs a current
1886
+ * one; the display layer's own refresh path is the adopt ladder. */
1887
+ private _refreshLiveIndexKeys;
1888
+ /**
1889
+ * Replace the live-index snapshot from a queue query's raw items.
1890
+ *
1891
+ * Whole-snapshot, never incremental: the query returns everything unresolved on
1892
+ * the queue, so a file MISSING from it is precisely the fact we are after. Merging
1893
+ * would make a finished file impossible to observe.
1894
+ */
1895
+ private _recordLiveIndexKeys;
1896
+ /** Forget the snapshot: it describes ONE chat's queue, and the answer for the
1897
+ * project the user just switched to is unknown until it is asked for again. */
1898
+ private _resetLiveIndexKeys;
1899
+ /**
1900
+ * Ask the queue what is still indexing, once, for the chat that is on screen.
1901
+ *
1902
+ * Seeds the snapshot on a history load. Without it a reloaded chat has no way to
1903
+ * learn that a run it can see is over: the adopt ladder that normally answers this
1904
+ * only fires when a pass SETTLES, and after a reload there is no pass left to
1905
+ * settle — so every finished worker-driven row would spin forever.
1906
+ *
1907
+ * Best-effort: a failure leaves `checked` false, which reads as "still working"
1908
+ * rather than as a false all-clear.
1909
+ *
1910
+ * Delegates to the adopt ladder rather than asking once. A single empty look is
1911
+ * exactly what that ladder exists to distrust — the worker writes pass N+1 a few
1912
+ * milliseconds AFTER flipping pass N to resolved, so a query landing in that gap
1913
+ * sees an empty queue for a chain that is very much alive. One look would turn
1914
+ * that into a confident "Indexed" with a green check, on the one scenario this
1915
+ * whole feature is for, and nothing would ever re-ask: the ladder is normally
1916
+ * triggered by a pass SETTLING, and after a reload there is no pass left to
1917
+ * settle. The ladder re-asks at 0/2s/6s, records each answer, and as a bonus
1918
+ * adopts and polls any live pass it finds, which makes the row genuinely active
1919
+ * instead of merely unconfirmed.
1920
+ */
1921
+ refreshLiveIndexState(): void;
1922
+ /** Forget what we know about which files are indexing — but ONLY when the
1923
+ * snapshot was taken for a different chat than the one on screen now. For a
1924
+ * consumer whose history loading is its own fork and so never reaches
1925
+ * loadHistory's reset — a snapshot describes ONE chat's queue, and carrying it
1926
+ * into another project would let a row there claim to be finished on someone
1927
+ * else's evidence.
1928
+ *
1929
+ * Conditional for the same reason loadHistory's own reset is (the
1930
+ * `loadKey !== _liveIndexKey` gate): the view calls this on every mount, and
1931
+ * an unconditional wipe turned every re-entry to the chat into a grey
1932
+ * "Checking status:" sweep across rows whose state was already known. A
1933
+ * RE-entry keeps showing the last answer (green/yellow) while the first-page
1934
+ * refresh re-asks quietly; only a genuine project/platform switch starts from
1935
+ * "not known yet". Claiming `_liveIndexKey` here (before any answer) is the
1936
+ * same fudge loadHistory makes: it marks WHOSE chat the empty snapshot is
1937
+ * for, so repeated calls do not re-wipe, and _recordLiveIndexKeys re-claims
1938
+ * it when the real answer lands. */
1939
+ resetLiveIndexState(): void;
1940
+ /** Wrap an indexing-request dispatch so awaitIndexingDrained counts it as
1941
+ * live work from the moment it is sent, not from the moment it is acked. */
1942
+ trackIndexDispatch<T>(p: Promise<T>): Promise<T>;
1943
+ /**
1944
+ * Something just happened that plausibly ENDED indexing work, so let any waiting
1945
+ * turn look now instead of sitting out the rest of its busy interval.
1946
+ *
1947
+ * A nudge changes only WHEN a look happens, never what it concludes: the two
1948
+ * agreeing idle looks, the confirm gap between them, "a failed look counts as
1949
+ * busy" and the minimum wait are all untouched. That is why it is safe to fire
1950
+ * from places that are merely good guesses.
1951
+ *
1952
+ * Fired from end-of-chain points ONLY: the adopt ladder giving up, a resume
1953
+ * declining to continue, a pass failing. Not from every settling pass (one nudge
1954
+ * per pass per file for the whole run), and not from an indexing request being
1955
+ * accepted — see the note in trackIndexDispatch for why that one is actively
1956
+ * harmful rather than merely wasteful.
1957
+ */
1958
+ private _nudgeIndexingDrain;
1242
1959
  /**
1243
1960
  * Register a live poll so (a) a remount dedupes against it instead of stacking a
1244
1961
  * SECOND poll on the same item, and (b) pausePolling can stop it.
@@ -1247,6 +1964,12 @@ declare class ChatSession {
1247
1964
  * poll simply cannot be stopped and is left running — see pausePolling.
1248
1965
  */
1249
1966
  private _trackPoll;
1967
+ /** Background polls currently attached, for the MAX_CONCURRENT_BG_POLLS budget.
1968
+ * Counts the registry rather than a separate tally so it cannot drift: every
1969
+ * attach goes through _trackPoll and every detach deletes the entry. Note an
1970
+ * entry left behind by pausePolling on an older skapi-js (no stop handle)
1971
+ * still counts, which is correct — that poll really is still running. */
1972
+ private _countBgPolls;
1250
1973
  /**
1251
1974
  * Stop and forget one item's poll. Used after a cancel: the row is either gone
1252
1975
  * (cancelled while queued) or flagged cancelled (cancelled while running), so
@@ -1292,7 +2015,7 @@ declare class ChatSession {
1292
2015
  */
1293
2016
  private _applyReplyToCache;
1294
2017
  /**
1295
- * serviceId/owner are passed explicitly by every caller: a request can be
2018
+ * projectId/owner are passed explicitly by every caller: a request can be
1296
2019
  * dispatched after the user moved to another project, and re-reading the live
1297
2020
  * identity here would silently send the turn to THAT project instead of the
1298
2021
  * one it was composed for. Falls back to the live read only when a caller
@@ -1300,9 +2023,106 @@ declare class ChatSession {
1300
2023
  */
1301
2024
  private _callProviderFor;
1302
2025
  dispatchAgentRequest(params: any): Promise<any>;
2026
+ /**
2027
+ * Put a turn on screen the INSTANT the user hits Send, before its attachments
2028
+ * have finished uploading. Uploads run in the background now (the composer is
2029
+ * cleared and stays usable), so without a staged bubble the message would
2030
+ * appear only once its files were up — below anything the user sent in the
2031
+ * meantime, in an order that never matches what they typed.
2032
+ *
2033
+ * Staged bubbles carry _useBgQueue because that is where a turn with
2034
+ * attachments ultimately dispatches (behind its own indexing tasks). That flag
2035
+ * is also what keeps promoteNextQueuedToRunning / resolveQueuedUserBubble off
2036
+ * them: those advance the SERVER queue, and a staged turn has no server
2037
+ * request behind it yet.
2038
+ *
2039
+ * Returns the id to hand back as PinnedDispatchContext.stageId at dispatch.
2040
+ */
2041
+ stageOutgoingMessage(displayText: string): string;
2042
+ /** Is anything in this page still uploading/dispatching for this stage? */
2043
+ isLiveStage(stageId?: string): boolean;
2044
+ /**
2045
+ * Settle any staged bubble in `list` whose chain no longer exists, and return the
2046
+ * list (a new array only if something changed).
2047
+ *
2048
+ * The caller is a cache restore. A staged bubble is the one kind of message whose
2049
+ * resolution lives entirely in page memory — no server request stands behind it
2050
+ * yet — so a copy that outlives its upload would render "(Uploading files...)"
2051
+ * forever with nothing left to finish it. Today nothing can: this cache dies with
2052
+ * the page, so every restored stage is still live and this is a no-op. It exists
2053
+ * so that stops being a silent assumption.
2054
+ */
2055
+ settleDeadStagedMessages(list: ChatMessage[]): ChatMessage[];
2056
+ private _stageIndex;
2057
+ /**
2058
+ * Staged turn, phase 2: its files are up and it is now waiting for the whole
2059
+ * indexing chain behind them. Swaps "(Uploading files...)" for
2060
+ * "(Indexing files...)"; the bubble stays dimmed, because from the user's side
2061
+ * nothing has been handed over yet.
2062
+ *
2063
+ * It deliberately does NOT say "(In queue)" here. The turn is not queued behind
2064
+ * anything the server knows about yet — it is waiting on work that can run for
2065
+ * minutes — and claiming otherwise is what made the wait look like a stall.
2066
+ */
2067
+ markStagedMessageIndexing(stageId: string): void;
2068
+ /**
2069
+ * Staged turn, phase 3: the last of its files has finished indexing, so the turn
2070
+ * is genuinely just queued now. Full opacity + "(In queue)".
2071
+ *
2072
+ * Clears the PRESENTATIONAL _dimSending only; isSendingToServer stays set until
2073
+ * the server actually acks (it is the token that ack matches on). Called by the
2074
+ * clients the instant awaitIndexingDrained resolves, i.e. immediately before the
2075
+ * dispatch that replaces this bubble — dispatchComposedMessage carries the
2076
+ * cleared flag onto the replacement so the turn does not blink back to dimmed.
2077
+ */
2078
+ markStagedMessageReady(stageId: string): void;
2079
+ /**
2080
+ * Resolves once this project's background-indexing queue has nothing left to
2081
+ * run, so a chat enqueued right after it is genuinely last.
2082
+ *
2083
+ * Sending the chat as soon as the uploads finish is not enough, which is the
2084
+ * whole reason this exists: indexing a file is a CHAIN, and each pass is only
2085
+ * enqueued once the previous one lands (the client mints CONTINUE passes for
2086
+ * text/grid files, the worker mints them for PDFs and windowed reads). Every
2087
+ * one of those passes therefore queues up BEHIND a chat sent at upload time,
2088
+ * and the model answers from a file it has only partly read.
2089
+ *
2090
+ * The queue is read from the server's status index rather than from
2091
+ * bgTaskQueue: that mirror holds only what this client dispatched or adopted,
2092
+ * and it stops being maintained once the view unmounts. An empty answer has to
2093
+ * repeat before it is believed — see INDEXING_DRAIN_IDLE_LOOKS — and a look
2094
+ * that fails counts as busy, so a dropped request delays the turn instead of
2095
+ * releasing it early.
2096
+ *
2097
+ * Reads the identity PINNED at Send time, never a live one: the user may be in
2098
+ * another project by now, and this must keep asking about the one they sent
2099
+ * from.
2100
+ */
2101
+ awaitIndexingDrained(identity: ChatIdentity): Promise<'drained' | 'timedout' | 'skipped'>;
2102
+ /**
2103
+ * Abandon a staged turn — its uploads failed outright, so nothing will be
2104
+ * dispatched. The bubble stays (the user's text is not silently thrown away)
2105
+ * but settles into a plain, non-pending message; the caller reports the
2106
+ * failure separately.
2107
+ */
2108
+ settleStagedMessage(stageId: string): void;
1303
2109
  dispatchComposedMessage(composed: string, useBgQueue?: boolean, composedForLlm?: string, extractContent?: any, fileUrls?: any, pinned?: PinnedDispatchContext): void;
1304
2110
  promoteNextBgQueuedToRunning(): void;
1305
2111
  promoteNextQueuedToRunning(): void;
2112
+ /**
2113
+ * The "Thinking..." placeholder belonging to the user bubble at `userIdx`, or -1.
2114
+ *
2115
+ * Every path that creates one puts it IMMEDIATELY after its user bubble
2116
+ * (promoteNextQueuedToRunning, the immediate-send pair, applyHistoryItemResolution),
2117
+ * so ownership is adjacency — modulo background bubbles, which get spliced in
2118
+ * around them. Taking the first pending assistant ANYWHERE below instead was a
2119
+ * hijack: a turn sent with attachments never gets a placeholder of its own
2120
+ * (promoteNextQueuedToRunning skips _useBgQueue turns) and now keeps the position
2121
+ * it was sent in, so an ordinary turn sent while its files indexed sits BELOW it
2122
+ * with a placeholder of its own — and the attachment turn's answer was rendered
2123
+ * as the answer to that unrelated question.
2124
+ */
2125
+ private _ownThinkingIndex;
1306
2126
  resolveQueuedUserBubble(serverId?: string): number | undefined;
1307
2127
  insertAtTarget(msg: ChatMessage, targetIdx: number): void;
1308
2128
  onQueuedSendResponse(_composed: string, response: any, platform: string, serverId?: string, ownerKey?: string): void;
@@ -1321,7 +2141,10 @@ declare class ChatSession {
1321
2141
  * 2. the file is remembered in cancelledIndexKeys, so the client-driven
1322
2142
  * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
1323
2143
  * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
1324
- * drain rather than surfacing a fresh "Indexing…" bubble.
2144
+ * drain rather than surfacing a fresh "Indexing…" bubble; and
2145
+ * 4. the RUN is remembered (state.stoppedIndexIds), because none of the above
2146
+ * necessarily leaves a mark on the conversation — see below — and without
2147
+ * it the collapsed row reported the stopped file as finished.
1325
2148
  *
1326
2149
  * Records already written by the passes that DID run are kept — this stops the
1327
2150
  * work, it does not undo it.
@@ -1332,12 +2155,48 @@ declare class ChatSession {
1332
2155
  enqueueTypewrite(idx: number, fullText: string, localId?: string): Promise<any>;
1333
2156
  typewriteLatestReply(key: string): Promise<any>;
1334
2157
  _removeStrayPendingAssistants(): void;
2158
+ /** Index of the USER bubble the message at `idx` belongs to — the nearest one
2159
+ * above it, stepping over background bubbles (a file's indexing rows are
2160
+ * inserted between turns). -1 when the nearest thing above is not a user turn,
2161
+ * which for a placeholder means it is an orphan. */
2162
+ private _owningUserIndex;
2163
+ /** The bubble at `idx` is the "Thinking…" of a DIFFERENT turn that is still
2164
+ * waiting for its answer, so the sweep above must leave it alone. */
2165
+ private _isLiveImmediatePlaceholder;
2166
+ /** A pending assistant at `idx` is the placeholder OF the turn above it, so a
2167
+ * reply may take its slot. Every path that makes one copies the parent's
2168
+ * _serverItemId (or neither has one yet), so a mismatch means the slot belongs to
2169
+ * some other request and the reply must be spliced in beside it, not on top. */
2170
+ private _isOwnPlaceholderOf;
1335
2171
  _clearPendingUserBubble(itemId: string): void;
1336
2172
  resumePendingRequest(token: number): Promise<void>;
1337
2173
  handleHistoryItemResolution(itemId: string, response: any, platform: string): void;
1338
2174
  /** The file an already-rendered background pass is about, off its request
1339
2175
  * bubble. Null for an ordinary turn, which is most of them. */
1340
2176
  private _indexRefOfItem;
2177
+ /**
2178
+ * Settle a turn the server reports as cancelled: the request bubble goes to its
2179
+ * cancelled form and the "Thinking..." placeholder goes away. The same shape
2180
+ * cancelQueuedMessage produces locally, so a cancel this client made and one it
2181
+ * merely found out about render identically — and an indexing pass keeps the
2182
+ * markers that hold it in its file's collapsed row.
2183
+ */
2184
+ private _settleCancelledItem;
2185
+ /**
2186
+ * A poll that came back saying the request was CANCELLED, rather than with an
2187
+ * answer.
2188
+ *
2189
+ * The server keeps a cancelled request as a terminal row instead of deleting it
2190
+ * (that row is the durable record of the stop, and the chat history it belongs
2191
+ * to), so a poll still running when the cancel lands now RESOLVES on it. It used
2192
+ * to reject with NOT_EXISTS, and the resolution path below reads a status object
2193
+ * as an answer with no text — which would stamp "No text response received from
2194
+ * AI provider" over a turn the user had just stopped.
2195
+ *
2196
+ * Reachable whenever the poll was not stopped by whoever cancelled: another tab,
2197
+ * another device, or the row being cancelled server-side by the file's own stop.
2198
+ */
2199
+ private _isCancelledPollResult;
1341
2200
  applyHistoryItemResolution(itemId: string, response: any, platform: string): void;
1342
2201
  /** How a bg task maps onto a collapsed row: the row's own key (storage path
1343
2202
  * when known, else the filename), scoped to the chat it belongs to. A storage
@@ -1353,6 +2212,16 @@ declare class ChatSession {
1353
2212
  * path, and without this an earlier cancel would silently kill every future
1354
2213
  * index of the same path. A continuation of a stopped file is dropped instead,
1355
2214
  * covering the pass that was dispatched in the moment before the cancel landed.
2215
+ *
2216
+ * "Fresh" is the load-bearing word, and it used to be missing. A run's OWN first
2217
+ * pass sits in this queue for as long as it runs (entries are only dropped once
2218
+ * their bubble settles), so stopping a file during its first pass — which is
2219
+ * exactly when a user who has just uploaded it does — met that first-pass entry
2220
+ * on the very next drain and lifted the stop the user had just asked for. The
2221
+ * chain then carried on, one worker-minted window after another, with nothing
2222
+ * client-side left to suppress it. The ids recorded at stop time are what tells
2223
+ * the two apart: a pass that was already there when the user hit Stop cannot be
2224
+ * the new request that lifts it.
1356
2225
  */
1357
2226
  private _applyIndexCancellations;
1358
2227
  /**
@@ -1418,12 +2287,12 @@ declare class ChatSession {
1418
2287
  drainBgTaskQueue(): void;
1419
2288
  maybeResumeIndexing(entry: BgTaskEntry, response: any, platform: string): void;
1420
2289
  loadHistory(fetchMore?: boolean, token?: number): Promise<void>;
1421
- uploadSingleAttachment(att: any): Promise<Array<{
2290
+ uploadSingleAttachment(att: any, stageId?: string): Promise<Array<{
1422
2291
  name: string;
1423
2292
  url: string;
1424
2293
  storagePath: string;
1425
2294
  }>>;
1426
- uploadPendingAttachments(): Promise<Array<{
2295
+ uploadPendingAttachments(batchId?: string, stageId?: string): Promise<Array<{
1427
2296
  name: string;
1428
2297
  url: string;
1429
2298
  storagePath?: string;
@@ -1432,4 +2301,4 @@ declare class ChatSession {
1432
2301
  bumpGate(): void;
1433
2302
  }
1434
2303
 
1435
- export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, type EncodingClass, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkPart, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type ParsedAiAgent, type PinnedDispatchContext, RENDER_FROM_TOKEN, RTF_EXTS, TOOL_AND_RESPONSE_BUFFER, XML_EXTS, applyEncodingDeclaration, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, groupAttachmentFailures, hasBom, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, prepareDownloadText, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };
2304
+ export { type AiAgentPlatform, type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, BOM, BOM_EXTS, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, CLAUDE_INPUT_CAP_RATIO, CLAUDE_PER_REQUEST_INPUT_CAP, CONTEXT_WINDOW_BY_MODEL, CONTEXT_WINDOW_DEFAULT, type CallClaudeWithMcpParams, type ChatEngineConfig, type ChatHost, type ChatIdentity, type ChatMessage, ChatSession, type ChatState, type ChatSystemPromptParams, type ClaudeMcpServerRequest, type ClaudeMcpToolConfig, type ClaudeMessage, type ClaudeRole, type ComposedUserMessage, DEFAULT_CLAUDE_MODEL, DEFAULT_OPENAI_MODEL, type DisplayEntry, EMPTY_INDEXING_REPLY, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, EXPIRED_LINK_REFRESH_EXPIRES_SECONDS, EXT_CONTENT_TYPES, type EncodingClass, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_BUDGET_RATIO, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, HTML_EXTS, HTML_HEAD_WINDOW, IMAGE_PREVIEWS_PER_MESSAGE, INDEXING_COMPLETE_MARKER, INLINE_LINK_GLYPH, INLINE_LINK_UNAVAILABLE_GLYPH, INLINE_LINK_UNAVAILABLE_SUFFIX, type ImagePreviewContext, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, type IndexingRequestRef, type IndexingSystemPromptParams, type InlineLinkContext, type InlineLinkMarkupOptions, type InlineLinkPart, LINK_LABEL_MAX_DISPLAY_CHARS, LINK_REFRESH_WINDOW_MS, MAX_CONCURRENT_BG_POLLS, MAX_HISTORY_FILL_PAGES, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, PREVIEWABLE_IMAGE_CONTENT_TYPES, PREVIEW_BROWSER_CACHE_SECONDS, type ParsedAiAgent, type PinnedDispatchContext, type PreviewImageEl, RENDER_FROM_TOKEN, RTF_EXTS, type RenderableInlineLink, TOOL_AND_RESPONSE_BUFFER, type VisionProfile, XML_EXTS, applyEncodingDeclaration, bgIndexingQueueName, buildAiAgentValue, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, clearImagePreviewCache, composeUserMessage, configureChatEngine, contentTypeForExt, createHistoryFiller, createInlineLinkRegex, encodePathSegments, encodingClassForExt, ensureHtmlCharset, ensureXmlEncoding, escapeInlineHtml, escapeRtfNonAscii, estimateMessageTokens, estimateTextTokens, extOf, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, getProjectContextWindow, getVisionProfile, groupAttachmentFailures, hasBom, hydrateImagePreviews, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isIndexingRequestText, isLinkUnavailable, isNonRetryableRequestError, isOfficeFile, isPreviewableImagePath, isServerExtractable, isServiceDbAttachmentHref, linkUnavailableKeyForHref, linkUnavailableKeyForPath, listClaudeModels, listOpenAIModels, looksLikeRtf, makeExtractPlaceholder, mapHistoryListToMessages, markImagePreviewStale, needsBomForExt, normalizeAttachmentPathCandidate, normalizeExt, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAiAgentValue, parseAttachmentContent, parseIndexingLabel, parseIndexingRequestText, peekImagePreviewUrl, prepareDownloadText, previewImageContentType, previewableExtOf, readExpiredAttachmentHref, registerAttachmentParser, registerModelContextWindows, renderInlineLinkHtml, repairUrlEntities, repairUrlWhitespace, resolveImagePreviewUrl, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, setProjectContextWindow, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };