bunnyquery 1.6.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.d.ts CHANGED
@@ -356,6 +356,26 @@ declare function buildBoundedChatMessages(options: BoundedChatOptions): {
356
356
  declare var EXPIRED_ATTACHMENT_URL_HOST: string;
357
357
  declare var EXPIRED_ATTACHMENT_URL_ORIGIN: string;
358
358
  declare var LINK_LABEL_MAX_DISPLAY_CHARS: number;
359
+ /**
360
+ * Lifetime of the url minted when a user clicks an expired attachment chip.
361
+ *
362
+ * Mint it as a PLAIN get-db presign, never with generate_temporary_cdn_url: the
363
+ * cdn branch ignores `expires` entirely and hands back a url good for the rest of
364
+ * the current UTC day plus the next one, so a "20 minute" link would in fact live
365
+ * 24 to 48 hours. The dashboard has always done this correctly and the widget did
366
+ * not, which is precisely the kind of divergence a shared constant exists to stop.
367
+ */
368
+ declare var EXPIRED_LINK_REFRESH_EXPIRES_SECONDS: number;
369
+ /**
370
+ * How long a client may keep serving an href it already minted before dropping
371
+ * back to the placeholder and re-minting.
372
+ *
373
+ * DERIVED from the TTL above, with five minutes of headroom, because the
374
+ * invariant "the cache must expire before the url does" used to be a comment
375
+ * next to two independent literals. If it is ever violated a client serves a
376
+ * dead url with no way to notice; deriving it makes that unrepresentable.
377
+ */
378
+ declare var LINK_REFRESH_WINDOW_MS: number;
359
379
  declare function createInlineLinkRegex(): RegExp;
360
380
  declare function safeDecodeURIComponent(v: string): string;
361
381
  declare function encodePathSegments(path: string): string;
@@ -364,9 +384,120 @@ declare function extractRemotePathFromAttachmentHref(href: string, serviceId: st
364
384
  declare function getExpiredAttachmentVisiblePath(remotePath: string, fallback?: string): string;
365
385
  declare function buildDisplayExpiredAttachmentHref(remotePath: string, fallback?: string): string;
366
386
  declare function isServiceDbAttachmentHref(href: string, serviceId: string): boolean;
387
+ /**
388
+ * Read the storage path back out of an `_expired_.url` placeholder.
389
+ *
390
+ * The placeholder is not a display detail: sanitizeAttachmentLinksForHistory
391
+ * writes it into PERSISTED history, and buildBoundedChatMessages replays it into
392
+ * the model's context. So it round-trips constantly and MUST be recognised on the
393
+ * way back in. Returns null for anything that is not the carrier.
394
+ */
395
+ declare function readExpiredAttachmentHref(href: string): string | null;
367
396
  declare function sanitizeAttachmentLinksForHistory(content: string, serviceId: string, forAssistant?: boolean): string;
397
+ /**
398
+ * Is this markdown link target a URL rather than a db storage path?
399
+ *
400
+ * The inline-link regex decides that by whether the target contains whitespace:
401
+ * its url branch forbids it, its bare-path branch allows it (a db path really can
402
+ * contain spaces). So a url that picked up a stray space anywhere in transit
403
+ * falls out of the url branch and is claimed by the path branch, and the view
404
+ * renders it as an `_expired_.url/https%3A/…` attachment chip that resolves to
405
+ * nothing. The view asks this FIRST, so what a link IS never depends on damage.
406
+ */
407
+ declare function isHttpUrlLike(target: string): boolean;
408
+ /**
409
+ * Repair whitespace inside a url. RFC 3986 has no legal whitespace anywhere in a
410
+ * URI, so a space in an href is always damage, never content.
411
+ *
412
+ * Two repairs, because the right one differs:
413
+ * - Our own `/download/<id>` capability links (skapi-mcp file-download.js) are
414
+ * base64url, optionally with a single `.` separating the payload and hmac of
415
+ * the older self-describing token. That alphabet cannot contain whitespace,
416
+ * so the spaces are purely damage and REMOVING them restores the exact link,
417
+ * which is what makes an already-sent message clickable again. A model
418
+ * reproducing one of these into its reply is exactly where the spaces come
419
+ * from, which is also why the id is now short.
420
+ * - Anything else keeps every character and only has the whitespace encoded,
421
+ * the same thing a browser does with a space in an href. Stripping would be
422
+ * wrong there: `…/exports/my report.csv` is a real file whose name has a
423
+ * space in it, and deleting it points at a file that does not exist.
424
+ */
425
+ declare function repairUrlWhitespace(href: string): string;
426
+ /**
427
+ * A model reproducing a URL sometimes HTML-escapes its `&` query separators as
428
+ * `&amp;` (or the numeric `&#38;` / `&#x26;`). Left in the href that escaping
429
+ * survives the client's own escapeAttr -> v-html/innerHTML decode round-trip and
430
+ * reaches the browser LITERALLY, so a presigned S3 URL is navigated with its
431
+ * parameters named `amp;Signature`, `amp;Expires`, `amp;response-content-type`,
432
+ * ... — the real params vanish, the signature can't be located, and S3 rejects
433
+ * it (the "링크가 안되" dead export link). Undo just that entity escaping.
434
+ *
435
+ * This is a no-op on a clean URL: a valid link carries a raw `&` between params
436
+ * and percent-encodes (`%26`) any literal ampersand that is data, so a real URL
437
+ * never contains `&amp;` to begin with. Mirrors repairUrlWhitespace: it repairs
438
+ * model damage, not the URL. The loop collapses a doubly-escaped `&amp;amp;` too.
439
+ */
440
+ declare function repairUrlEntities(href: string): string;
441
+ /**
442
+ * Trim punctuation and unmatched wrappers that cling to a token in prose.
443
+ * `src::a/b.pdf).` -> `src::a/b.pdf`, while a balanced `file (v2).pdf` is kept.
444
+ */
445
+ declare function normalizeTrailingInlineToken(value: string): string;
446
+ /** A link the view renders. `expired` means the href is the `_expired_.url`
447
+ * placeholder and a click must mint a fresh one from `remotePath`. */
448
+ interface InlineLinkPart {
449
+ type: 'link';
450
+ label: string;
451
+ fullLabel: string;
452
+ href: string;
453
+ expired: boolean;
454
+ expiredHref?: string;
455
+ remotePath?: string;
456
+ }
457
+ interface InlineLinkContext {
458
+ /** Current project id: the leading segment to strip off a db url. */
459
+ serviceId: string;
460
+ /** `https://db.<hostDomain>` for this deployment. */
461
+ dbHostPrefix: string;
462
+ /** A fresh url already minted for this placeholder, if the view cached one. */
463
+ resolveFreshHref?: (expiredHref: string) => string | undefined;
464
+ }
465
+ /**
466
+ * Decide what ONE inline-link regex match actually is, and how to render it.
467
+ *
468
+ * This is the single place that answers "is this an external url, this project's
469
+ * db file, or a bare storage path", for every consumer. It used to live twice,
470
+ * once in agent.vue and once in the widget, and both copies had to be found and
471
+ * corrected for each of the link bugs this file's history records. A view now
472
+ * supplies its own context (project id, db host, cached-href lookup) and does
473
+ * nothing but turn the returned part into markup.
474
+ *
475
+ * `groups` is [g1..g6] from createInlineLinkRegex, in that order:
476
+ * g1 src::<token> g2/g3 [label](url) g4/g5 [label](path) g6 bare url
477
+ */
478
+ declare function classifyInlineLink(full: string, groups: Array<string | undefined>, ctx: InlineLinkContext): {
479
+ part: InlineLinkPart;
480
+ tail?: string;
481
+ } | null;
368
482
  declare function truncateLabelForDisplay(label: string): string;
369
483
 
484
+ /**
485
+ * Chat timestamp formatting, shared so agent.vue and the widget render an
486
+ * identical "small text under the bubble". Pure and locale-aware: it formats a
487
+ * given epoch-ms value, it never reads the current time, so it stays testable and
488
+ * DOM-free.
489
+ */
490
+ /** Wall-clock epoch ms. Separate from the engine's monotonic nowMs() (which is
491
+ * performance.now() when available and therefore NOT epoch): a displayed
492
+ * timestamp must be wall time. */
493
+ declare function wallClockNow(): number;
494
+ /**
495
+ * "Jul 24, 2026, 3:42:07 PM" (locale-formatted). Empty string for a missing or
496
+ * non-finite value, so a caller can gate rendering on the result being truthy and
497
+ * a pending bubble (no timestamp yet) simply shows nothing.
498
+ */
499
+ declare function formatChatTimestamp(ms?: number): string;
500
+
370
501
  declare function filterListByClearHorizon(list: any[], clearedAt: number): any[];
371
502
  declare function normalizeTextContent(content: any): string;
372
503
  declare function extractLastUserTextFromRequest(requestBody: any): string;
@@ -374,16 +505,107 @@ type MapHistoryOptions = {
374
505
  clearedAt: number;
375
506
  serviceId: string;
376
507
  /** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
377
- formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string) => string;
508
+ formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean) => string;
378
509
  };
379
510
  declare function mapHistoryListToMessages(list: any[], platform: 'claude' | 'openai', opts: MapHistoryOptions): {
380
511
  messages: any[];
381
512
  runningItemIds: string[];
382
513
  };
383
514
 
515
+ /**
516
+ * Keep older history REACHABLE by paging until the message box actually gains
517
+ * something to scroll to.
518
+ *
519
+ * Older history is paged in by one trigger only: the user scrolling to the top
520
+ * of the message box. That trigger has two ways to die, and collapsed indexing
521
+ * rows cause both:
522
+ *
523
+ * 1. The box never scrolls. A file's every indexing pass (the first plus every
524
+ * CONTINUE pass, request AND response bubble each) folds into ONE row, so a
525
+ * full history page — twenty-plus messages — can render as a single line.
526
+ * Content shorter than the viewport fires no scroll event, so page 2 is
527
+ * never requested and any conversation the user had before that upload is
528
+ * permanently out of reach.
529
+ * 2. The fetched page adds no height. A page that is entirely the same file's
530
+ * earlier passes joins the collapsed row already on screen and renders
531
+ * nothing new. The user, sitting at scrollTop 0, scrolls up again — and
532
+ * because the position never changed, no further scroll event fires.
533
+ *
534
+ * Both are the same shape: fetch, re-measure, and keep going until the user
535
+ * genuinely gained reachable content, history ran out, or the pager stopped
536
+ * advancing. `isSatisfied` is what differs between the two (can the box scroll
537
+ * at all / did it grow), so the loop below takes it as a predicate.
538
+ *
539
+ * DOM-free like the rest of the engine — the caller supplies the measurement and
540
+ * awaits its own render before measuring, so agent.vue and the widget run the
541
+ * identical loop over their own pagers.
542
+ */
543
+ /** Overflow (px) that counts as "the user can scroll here". Comfortably more
544
+ * than the 60px top threshold that triggers the next page, so a filled box has
545
+ * real room to scroll rather than sitting one pixel from the trigger. */
546
+ declare const HISTORY_FILL_SLACK_PX = 64;
547
+ /** Pages one fill pass will request before giving up. Reached only by a chat
548
+ * whose history really is dozens of pages of one file's indexing passes; the
549
+ * cap exists so a pager that stops advancing can never spin forever. */
550
+ declare const MAX_HISTORY_FILL_PAGES = 24;
551
+ type FillHistoryViewportOptions = {
552
+ /** The user has reachable content and paging can stop. Called AFTER the
553
+ * caller's own render has settled (nextTick / rAF), since only the caller
554
+ * knows when its view has painted — hence the allowance for a promise. */
555
+ isSatisfied: () => boolean | Promise<boolean>;
556
+ /** All history is loaded — nothing left to page in. */
557
+ isEndOfList: () => boolean;
558
+ /** A history request is already in flight. Waited out, not treated as a stop
559
+ * condition: a background first-page refresh (the queue-detect tick fires one
560
+ * every couple of seconds while a file is indexing) would otherwise swallow
561
+ * the user's scroll-up entirely, and scrolling up again from scrollTop 0
562
+ * produces no second event to retry with. */
563
+ isLoading: () => boolean;
564
+ /** Messages currently loaded. Used to detect a page that added nothing, which
565
+ * means the pager is not advancing and looping would never terminate. */
566
+ messageCount: () => number;
567
+ /** Fetch ONE older page (the caller's own fetchMore path, scroll-restore and
568
+ * all). Return `false` when the request was NOT issued (the caller's own
569
+ * single-flight guard swallowed it) so the loop retries instead of reading
570
+ * the unchanged message count as an exhausted pager. Anything else, including
571
+ * undefined, means it was attempted. */
572
+ fetchOlder: () => Promise<boolean | void | any>;
573
+ /** The chat this fill was started for is gone (project switched, view
574
+ * unmounted, gate token bumped). Checked between pages so a stale fill can
575
+ * never keep paging another chat's history. */
576
+ isStale?: () => boolean;
577
+ maxPages?: number;
578
+ };
579
+ /**
580
+ * Page older history until `isSatisfied`, until history runs out, or until the
581
+ * pager stops advancing. Never throws: a failed page ends the fill, and the
582
+ * user's own scrolling remains the fallback trigger.
583
+ */
584
+ declare function fillHistoryViewport(opts: FillHistoryViewportOptions): Promise<void>;
585
+ /**
586
+ * One fill loop per view, with predicates COMBINED rather than dropped.
587
+ *
588
+ * Fills come from several places at once — a first page finishing, a window
589
+ * resize, a row being collapsed, and the user's own scroll to the top — and a
590
+ * plain "one at a time, drop the rest" guard picks the wrong winner: a resize
591
+ * fill (satisfied the moment the box can scroll at all) would swallow the user's
592
+ * scroll-up (which needs content specifically ABOVE them), and the scroll-up
593
+ * cannot be retried, because a reader parked at scrollTop 0 produces no further
594
+ * scroll event. Dropping the guard entirely is no better: every frame of a
595
+ * window drag would start its own 24-page loop.
596
+ *
597
+ * So a request that arrives mid-loop ANDs its predicate into the running one:
598
+ * the loop then keeps paging until EVERY caller is satisfied. Predicates that
599
+ * come true are dropped as it goes, so the cost stays flat.
600
+ */
601
+ declare function createHistoryFiller(base: Omit<FillHistoryViewportOptions, 'isSatisfied'>): {
602
+ fill: (isSatisfied: () => boolean | Promise<boolean>) => Promise<void>;
603
+ isRunning: () => boolean;
604
+ };
605
+
384
606
  declare const MCP_NAME = "BunnyQuery";
385
607
  declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
386
- declare const DEFAULT_OPENAI_MODEL = "gpt-5.4";
608
+ declare const DEFAULT_OPENAI_MODEL = "gpt-5.6-luna";
387
609
  type ClaudeRole = 'user' | 'assistant';
388
610
  type ClaudeMessage = {
389
611
  role: ClaudeRole;
@@ -527,6 +749,24 @@ interface PinnedDispatchContext {
527
749
  identity: ChatIdentity;
528
750
  systemPrompt: string;
529
751
  }
752
+ /**
753
+ * The file a background-indexing bubble belongs to. Stamped on the REQUEST
754
+ * bubble (both the live one and the one rebuilt from history) so the display
755
+ * layer can group a file's many passes without reverse-parsing the view's
756
+ * formatted label. See indexing_groups.buildChatDisplayList.
757
+ */
758
+ interface IndexingFileRef {
759
+ name: string;
760
+ /** Storage path, when known. Preferred group identity: a file can be
761
+ * re-uploaded under a name that already exists elsewhere. */
762
+ path?: string;
763
+ mime?: string;
764
+ size?: number;
765
+ isReindex?: boolean;
766
+ /** A CONTINUE pass. Its absence across every loaded pass is what tells the
767
+ * display layer that earlier passes are still unpaged. */
768
+ continued?: boolean;
769
+ }
530
770
  interface ChatMessage {
531
771
  role: 'user' | 'assistant';
532
772
  content: string;
@@ -538,11 +778,19 @@ interface ChatMessage {
538
778
  isCancelled?: boolean;
539
779
  isError?: boolean;
540
780
  isBackgroundTask?: boolean;
781
+ /** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
782
+ _indexFile?: IndexingFileRef;
541
783
  _useBgQueue?: boolean;
542
784
  _serverItemId?: string;
543
785
  _localId?: string;
544
786
  _cancelling?: boolean;
545
787
  _cancelError?: string;
788
+ /** Epoch ms shown as small text under the bubble. From the request history a
789
+ * USER bubble carries the request's `created` time and an ASSISTANT bubble the
790
+ * `updated` (response) time; a live bubble is stamped with the wall clock when
791
+ * it is created, then reconciled to the server value on the next history load.
792
+ * Absent while a turn is still pending, so no time shows on a "Thinking" bubble. */
793
+ _ts?: number;
546
794
  _ownerKey?: string;
547
795
  }
548
796
  interface ChatState {
@@ -573,6 +821,12 @@ interface ChatHost {
573
821
  scrollToBottom(smooth?: boolean): Promise<void> | void;
574
822
  /** Scroll only if the user is pinned to the bottom (does not force-pin). */
575
823
  scrollToBottomIfSticky(smooth?: boolean): Promise<void> | void;
824
+ /** A history page finished loading and rendering. OPTIONAL. The view uses it
825
+ * to page further when the message box came out too short to scroll — the
826
+ * only trigger for loading older history is a scroll to the top, so a box
827
+ * that cannot scroll has no way to reach page 2 (see viewport_fill). Only
828
+ * the view can measure that, which is why the engine merely announces it. */
829
+ onHistoryLoaded?(fetchMore: boolean, token: number): void;
576
830
  cancelRequest(opts: {
577
831
  url: string;
578
832
  method: string;
@@ -586,7 +840,7 @@ interface ChatHost {
586
840
  } | any>;
587
841
  refreshSession(): Promise<boolean>;
588
842
  /** Build the "Indexing:/Reindexing: …" label (view-side display formatting). */
589
- formatIndexingLabel(name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean): string;
843
+ formatIndexingLabel(name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean): string;
590
844
  /** drainBgTaskQueue is a no-op until the chat view is mounted. */
591
845
  isViewMounted(): boolean;
592
846
  /** Clear-horizon timestamp (localStorage, per service#platform) — view-owned. */
@@ -620,6 +874,136 @@ interface ChatHost {
620
874
  updateComposerControls(): void;
621
875
  }
622
876
 
877
+ /**
878
+ * Background file-indexing turns, collapsed into ONE row per file.
879
+ *
880
+ * A single upload can produce many chat turns: the first "Indexing: <file>" pass
881
+ * plus up to MAX_INDEXING_RESUME_PASSES CONTINUE passes, each with its own
882
+ * request AND response bubble. Rendered flat, that reads as the same task
883
+ * repeating forever, and any real question the user asks in between gets buried.
884
+ *
885
+ * buildChatDisplayList turns the flat message array into a DISPLAY list in which
886
+ * every message belonging to one run of one file (however far apart the passes
887
+ * sit, and whatever else is interleaved between them) is represented by a single
888
+ * group entry, rendered at the position of that run's FIRST loaded pass.
889
+ *
890
+ * First, not newest. The message array is ordered by request CREATION time, and
891
+ * a run's later passes are created one at a time as the previous one resolves —
892
+ * by the client for the paged text path, and by the WORKER itself for the
893
+ * rendered-page (PDF) path, which the client only learns about on its next
894
+ * first-page history fetch. So a pass is routinely created minutes after the
895
+ * upload, on a queue that runs in parallel with the foreground chat. Anchoring
896
+ * at the newest pass meant one such late pass dragged the whole run — every
897
+ * earlier pass with it — below any question the user had asked in the meantime,
898
+ * and made a run that had visibly finished before the question render after it.
899
+ * Anchoring at the first pass puts the row where the run actually began, which
900
+ * is a position no later pass can change:
901
+ * - a new pass never relocates the row, so nothing under the reader shifts;
902
+ * - a question asked after the upload always renders below the row, which is
903
+ * the order it happened in.
904
+ * The cost is that a long-running index does not follow the conversation to the
905
+ * bottom: once the user has chatted past the upload, the spinner and the Stop
906
+ * button sit above their newest turns. That is a scroll away, and the row no
907
+ * longer lies about when the work started.
908
+ *
909
+ * Paging older history is the one thing that CAN move the row: an older page
910
+ * carrying earlier passes of a run already on screen moves it up into that page.
911
+ * That happens at most once per run, only for a run whose start was never
912
+ * loaded (`mayHaveOlder`), and it is the same event that already re-derives
913
+ * `runKey` — so the view treats it as a new row either way.
914
+ *
915
+ * The group deliberately reports no authoritative pass TOTAL. History is paged
916
+ * newest-first, so any total computed from loaded messages is a lower bound that
917
+ * a later scroll-up would contradict. It reports STATE (indexing / indexed /
918
+ * failed), how many passes are currently loaded, and `mayHaveOlder` when the
919
+ * file's first pass is not among them.
920
+ *
921
+ * Pure and view-agnostic: agent.vue and the BunnyQuery widget both render from
922
+ * this, so the two stay identical.
923
+ */
924
+
925
+ type IndexingGroupStatus = 'active' | 'done' | 'error' | 'cancelled';
926
+ type IndexingGroup = {
927
+ /** The FILE this row is about: storage path when known (a file can be
928
+ * re-uploaded under a name that already exists elsewhere), else name. Shared
929
+ * by every run of that file, and what ChatSession.cancelIndexingGroup and
930
+ * _indexKeyOf match on — never use it as a render key. */
931
+ key: string;
932
+ /** Identity of this ROW: one indexing RUN of that file. A file indexed on
933
+ * Monday and re-indexed on Wednesday is two runs, and collapsing them into
934
+ * one row erased Monday's from Monday's place in the conversation, claimed
935
+ * its passes for Wednesday, and let Monday's failure be overwritten by
936
+ * Wednesday's success. Named after the run's FIRST loaded pass (see where it
937
+ * is assigned below), so passes appended to the run and other runs appearing
938
+ * on either side of it never rename a row already on screen. This is the
939
+ * render key and the expansion key. */
940
+ runKey: string;
941
+ name: string;
942
+ path?: string;
943
+ mime?: string;
944
+ size?: number;
945
+ /** True when any loaded pass was a re-index of an already-stored file. */
946
+ isReindex: boolean;
947
+ /** Every message of this file, in chat order, with its index in the source
948
+ * array (so cancel/typewriter paths keep addressing the real message). */
949
+ members: {
950
+ msg: ChatMessage;
951
+ index: number;
952
+ }[];
953
+ /** Indexing passes LOADED (request bubbles), never a server-side total. */
954
+ passCount: number;
955
+ status: IndexingGroupStatus;
956
+ /** Server item ids of the passes that are still queued/running, so the row can
957
+ * offer a stop button (ChatSession.cancelIndexingGroup cancels each). Empty
958
+ * when nothing is cancellable — a finished file, or a live pass whose server
959
+ * id has not come back yet. */
960
+ cancellableIds: string[];
961
+ /** A cancel request is in flight for one of the passes. */
962
+ cancelling: boolean;
963
+ /** Why the last cancel attempt failed (e.g. the pass had already finished). */
964
+ cancelError?: string;
965
+ /** The file's first pass is not among the loaded messages, so earlier passes
966
+ * exist in history that has not been paged in yet. */
967
+ mayHaveOlder: boolean;
968
+ /** Position in the source array this collapsed row renders at: the index of
969
+ * the run's FIRST loaded pass (see the file docstring for why not the last). */
970
+ anchorIndex: number;
971
+ /** Identity of the turn at `anchorIndex` — its server item id, or its local id
972
+ * while it has none, or `''` when it has neither. The views stamp this on the
973
+ * row (`data-row-pos`) so the scroll anchor can tell a row that RELOCATED (an
974
+ * older page moved the run's start) from one that merely gained a pass. They
975
+ * must not re-derive it from `members`: which member the row renders at is
976
+ * this module's decision, and the two silently disagreed once already. */
977
+ anchorId: string;
978
+ };
979
+ type DisplayEntry = {
980
+ kind: 'message';
981
+ msg: ChatMessage;
982
+ index: number;
983
+ } | {
984
+ kind: 'indexing';
985
+ group: IndexingGroup;
986
+ index: number;
987
+ };
988
+ type BuildDisplayListOptions = {
989
+ /** True while older history remains unpaged, which is what makes a group
990
+ * with no first pass genuinely incomplete rather than merely odd. */
991
+ hasMoreHistory?: boolean;
992
+ };
993
+ declare function parseIndexingLabel(content: string): {
994
+ name: string;
995
+ path?: string;
996
+ continued: boolean;
997
+ isReindex: boolean;
998
+ } | null;
999
+ /**
1000
+ * Collapse background-indexing turns into per-file groups.
1001
+ *
1002
+ * Messages that are not background-indexing pass through untouched, at their
1003
+ * original positions and with their original indices.
1004
+ */
1005
+ declare function buildChatDisplayList(messages: ChatMessage[], opts?: BuildDisplayListOptions): DisplayEntry[];
1006
+
623
1007
  /**
624
1008
  * ChatSession — framework-agnostic stateful chat orchestration.
625
1009
  *
@@ -652,6 +1036,13 @@ declare class ChatSession {
652
1036
  state: ChatState;
653
1037
  bgTaskQueue: BgTaskEntry[];
654
1038
  cancelledServerIds: Set<string>;
1039
+ /** Files whose indexing the user stopped, keyed exactly as buildChatDisplayList
1040
+ * keys a group (storage path, else filename). Cancelling one pass is not
1041
+ * enough on its own: the client dispatches CONTINUE passes for paged files, so
1042
+ * without this the next pass is enqueued the moment the cancelled one settles.
1043
+ * Cleared when a FIRST pass for the same file is queued again (a re-upload or a
1044
+ * Reindex from the file manager), so stopping a file never poisons it. */
1045
+ cancelledIndexKeys: Set<string>;
655
1046
  pendingAgentRequests: Record<string, Promise<any>>;
656
1047
  aiChatHistoryCache: Record<string, {
657
1048
  messages: ChatMessage[];
@@ -673,6 +1064,14 @@ declare class ChatSession {
673
1064
  * poll simply cannot be stopped and is left running — see pausePolling.
674
1065
  */
675
1066
  private _trackPoll;
1067
+ /**
1068
+ * Stop and forget one item's poll. Used after a cancel: the row is either gone
1069
+ * (cancelled while queued) or flagged cancelled (cancelled while running), so
1070
+ * asking about it again only burns requests. Safe when no poll is attached, and
1071
+ * safe on an older skapi-js with no stop handle (the entry is then LEFT in the
1072
+ * map so a later drain cannot stack a second, unstoppable poll on the id).
1073
+ */
1074
+ private _stopPoll;
676
1075
  /** True while any pause reason is active. */
677
1076
  isPollingPaused(): boolean;
678
1077
  /**
@@ -726,6 +1125,25 @@ declare class ChatSession {
726
1125
  onQueuedSendResponse(_composed: string, response: any, platform: string, serverId?: string, ownerKey?: string): void;
727
1126
  onQueuedSendError(_composed: string, err: any, serverId?: string, ownerKey?: string): void;
728
1127
  cancelQueuedMessage(msg: ChatMessage, idx: number): void;
1128
+ /**
1129
+ * Stop indexing a file, from its collapsed row — every pass at once, not just
1130
+ * the bubble the user happens to see.
1131
+ *
1132
+ * A big file is indexed as a CHAIN of passes, so cancelling only the live one
1133
+ * accomplishes nothing: the next pass is dispatched as soon as it settles.
1134
+ * Three things end the chain:
1135
+ * 1. every queued/running pass of this file is cancelled server-side
1136
+ * (csr-cancel deletes a queued row and flags a running one "cancelled",
1137
+ * which is also the worker's gate for NOT enqueueing the next window);
1138
+ * 2. the file is remembered in cancelledIndexKeys, so the client-driven
1139
+ * resume (maybeResumeIndexing) stops dispatching CONTINUE passes; and
1140
+ * 3. any of its passes still sitting in bgTaskQueue is dropped by the next
1141
+ * drain rather than surfacing a fresh "Indexing…" bubble.
1142
+ *
1143
+ * Records already written by the passes that DID run are kept — this stops the
1144
+ * work, it does not undo it.
1145
+ */
1146
+ cancelIndexingGroup(group: IndexingGroup): void;
729
1147
  typewriteIntoIndex(idx: number, fullText: string, localId?: string): Promise<void>;
730
1148
  private typewriterQueue;
731
1149
  enqueueTypewrite(idx: number, fullText: string, localId?: string): Promise<any>;
@@ -735,6 +1153,39 @@ declare class ChatSession {
735
1153
  resumePendingRequest(token: number): Promise<void>;
736
1154
  handleHistoryItemResolution(itemId: string, response: any, platform: string): void;
737
1155
  applyHistoryItemResolution(itemId: string, response: any, platform: string): void;
1156
+ /** How a bg task maps onto a collapsed row: the row's own key (storage path
1157
+ * when known, else the filename), scoped to the chat it belongs to. A storage
1158
+ * path is project-relative ("report.xlsx"), and ONE ChatSession serves every
1159
+ * project — unscoped, stopping a file in one project would silently suppress
1160
+ * the same filename's continuations in another. */
1161
+ private _indexKeyOf;
1162
+ /**
1163
+ * Reconcile the bg queue with the files the user has stopped.
1164
+ *
1165
+ * A FIRST pass (no resumePass) is a fresh indexing request — a re-upload, or a
1166
+ * Reindex from the file manager — so it LIFTS the stop: the key is a storage
1167
+ * path, and without this an earlier cancel would silently kill every future
1168
+ * index of the same path. A continuation of a stopped file is dropped instead,
1169
+ * covering the pass that was dispatched in the moment before the cancel landed.
1170
+ */
1171
+ private _applyIndexCancellations;
1172
+ /**
1173
+ * Cancel any live pass of a stopped file that turned up on its own.
1174
+ *
1175
+ * The client is not the only thing that continues a file: for PDFs and (when
1176
+ * windowed indexing is on) text/grid files the WORKER enqueues the next window
1177
+ * itself, and that pass reaches the chat through the history poll, never
1178
+ * through bgTaskQueue. The worker's own gate stops the chain when the running
1179
+ * row is cancelled — but if the row had already finished when the user hit
1180
+ * stop, the next window was queued a moment earlier and still arrives. Stop it
1181
+ * here rather than making the user hit stop again.
1182
+ *
1183
+ * Runs from drainBgTaskQueue, which both clients call after a history load.
1184
+ */
1185
+ private _sweepCancelledIndexing;
1186
+ /** Best-effort server-side cancel of a bg-queue item that has no bubble (so
1187
+ * cancelQueuedMessage, which drives one, has nothing to act on). */
1188
+ private _cancelServerItem;
738
1189
  drainBgTaskQueue(): void;
739
1190
  maybeResumeIndexing(entry: BgTaskEntry, response: any, platform: string): void;
740
1191
  loadHistory(fetchMore?: boolean, token?: number): Promise<void>;
@@ -752,4 +1203,4 @@ declare class ChatSession {
752
1203
  bumpGate(): void;
753
1204
  }
754
1205
 
755
- export { type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildIndexingUserMessageOptions, 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, EXPIRED_ATTACHMENT_URL_HOST, EXPIRED_ATTACHMENT_URL_ORIGIN, type ExtractDirective, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingSystemPromptParams, LINK_LABEL_MAX_DISPLAY_CHARS, MAX_HISTORY_MESSAGES, MAX_PARSED_CONTENT_CHARS, MCP_NAME, MIN_INPUT_TOKEN_BUDGET, type MapHistoryOptions, OUTPUT_TOKEN_RESERVE, type OpenAIMessage, POLL_INTERVAL, type PinnedDispatchContext, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, clearAttachmentParsers, composeUserMessage, configureChatEngine, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, filterListByClearHorizon, findAttachmentParser, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, notifyAgentSaveAttachment, parseAttachmentContent, registerAttachmentParser, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay };
1206
+ export { type AttachmentFailureGroup, type AttachmentParser, type AttachmentSaveInfo, BG_INDEXING_QUEUE_SUFFIX, type BgTaskEntry, type BoundedChatOptions, type BuildDisplayListOptions, type BuildIndexingUserMessageOptions, 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, type ExtractDirective, type FillHistoryViewportOptions, HISTORY_FILL_SLACK_PX, HISTORY_TOKEN_BUDGET, type IndexingAttachmentInfo, type IndexingFileRef, type IndexingGroup, type IndexingGroupStatus, 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 PinnedDispatchContext, RENDER_FROM_TOKEN, TOOL_AND_RESPONSE_BUFFER, buildBoundedChatMessages, buildChatDisplayList, buildChatSystemPrompt, buildDisplayExpiredAttachmentHref, buildIndexingContinueMessage, buildIndexingRenderContinueTemplate, buildIndexingRenderMessage, buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingWindowMessage, callClaudeWithMcp, callClaudeWithPublicMcp, callOpenAIWithPublicMcp, chatEngineConfig, classifyInlineLink, clearAttachmentParsers, composeUserMessage, configureChatEngine, createHistoryFiller, createInlineLinkRegex, encodePathSegments, estimateMessageTokens, estimateTextTokens, extractClaudeText, extractLastUserTextFromRequest, extractOpenAIText, extractRemotePathFromAttachmentHref, fillHistoryViewport, filterListByClearHorizon, findAttachmentParser, formatChatTimestamp, getAttachmentParsers, getChatHistory, getContextWindow, getErrorMessage, getExpiredAttachmentVisiblePath, groupAttachmentFailures, isAuthExpiredError, isBgIndexingQueue, isErrorResponseBody, isHttpUrlLike, isNonRetryableRequestError, isOfficeFile, isServerExtractable, isServiceDbAttachmentHref, listClaudeModels, listOpenAIModels, makeExtractPlaceholder, mapHistoryListToMessages, normalizeAttachmentPathCandidate, normalizeTextContent, normalizeTrailingInlineToken, notifyAgentSaveAttachment, parseAttachmentContent, parseIndexingLabel, readExpiredAttachmentHref, registerAttachmentParser, repairUrlEntities, repairUrlWhitespace, safeDecodeURIComponent, sanitizeAttachmentLinksForHistory, stripFileBlocksFromHistory, transformContentWithImages, transformContentWithOpenAIImages, truncateLabelForDisplay, wallClockNow };