bunnyquery 1.9.1 → 1.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,7 +46,7 @@ import { isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError, ge
46
46
  import { buildBoundedChatMessages } from './budget';
47
47
  import { createInlineLinkRegex, sanitizeAttachmentLinksForHistory } from './links';
48
48
  import { markImagePreviewStale } from './image_preview';
49
- import { mapHistoryListToMessages, extractLastUserTextFromRequest, isIndexingRequestText, parseIndexingRequestText, probeBgQueue, BG_PROBE_TTL_MS, getSplitChatHistory, shouldRescueInFlightMessage } from './history';
49
+ import { chatCacheKey, indexScopeKey, mapHistoryListToMessages, extractLastUserTextFromRequest, isIndexingRequestText, parseIndexingRequestText, probeBgQueue, BG_PROBE_TTL_MS, getSplitChatHistory, shouldRescueInFlightMessage } from './history';
50
50
  import { wallClockNow } from './time';
51
51
  import { parseAttachmentContent } from './attachment_parsers';
52
52
  import type { ChatHost, ChatState, ChatMessage, ChatIdentity, PinnedDispatchContext } from './host';
@@ -325,7 +325,8 @@ export class ChatSession {
325
325
  /** Storage paths are project-relative, and one ChatSession serves every
326
326
  * project, so a claim has to be scoped the way a stop is (_indexKeyOf). */
327
327
  private _indexClaimKey(storagePath: string): string {
328
- return this.getHistoryCacheKey() + '|' + storagePath;
328
+ var id = this.host.getIdentity();
329
+ return indexScopeKey(id.projectId, id.platform) + '|' + storagePath;
329
330
  }
330
331
 
331
332
  /**
@@ -798,10 +799,22 @@ export class ChatSession {
798
799
  return 'lid_' + this._lidSeq;
799
800
  }
800
801
 
802
+ /**
803
+ * The key every per-chat cache hangs off: the restored message cache, the
804
+ * hydrated-body memo, the live-index key and the per-file storage-path key.
805
+ *
806
+ * It carries the IDENTITY as well as the project and platform. A single
807
+ * browser can hold more than one conversation on one project without a
808
+ * reload — an anonymous visitor who signs in, or a dashboard user who logs
809
+ * out and back in as someone else — and with an identity-free key the
810
+ * previous conversation stayed in the cache and was re-rendered, and written
811
+ * back, as the new one's. `userId` is the same value the request queue is
812
+ * named after, so two identities that share a queue share a cache, which is
813
+ * exactly right.
814
+ */
801
815
  getHistoryCacheKey(): string {
802
816
  var id = this.host.getIdentity();
803
- if (!id.projectId || id.platform === 'none') return '';
804
- return id.projectId + '#' + id.platform;
817
+ return chatCacheKey(id.projectId, id.platform, id.userId);
805
818
  }
806
819
 
807
820
  // ─── compact-stub hydration ─────────────────────────────────────────────
@@ -1059,9 +1072,17 @@ export class ChatSession {
1059
1072
  if (projectId === undefined) projectId = id.projectId;
1060
1073
  if (owner === undefined) owner = id.owner;
1061
1074
  }
1075
+ // Read LIVE, unlike projectId/owner above, and deliberately: this decides
1076
+ // which MCP endpoint the turn's tools point at, and a retry after the
1077
+ // visitor signed in should use the authenticated one rather than the
1078
+ // project-scoped anonymous route it was composed under. Getting it wrong
1079
+ // costs a failed turn, never a wider read: the anonymous route is
1080
+ // read-only and public-records-only whoever calls it.
1081
+ var liveId = this.host.getIdentity();
1082
+ var mcpScope = { anonymous: liveId.anonymous, publicProjectId: liveId.publicProjectId };
1062
1083
  return platform === 'openai'
1063
- ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls)
1064
- : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls);
1084
+ ? callOpenAIWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, undefined, undefined, mcpScope)
1085
+ : callClaudeWithPublicMcp(prompt, projectId, owner, messages, system, model, userId, extractContent, fileUrls, undefined, undefined, mcpScope);
1065
1086
  }
1066
1087
 
1067
1088
  dispatchAgentRequest(params: any) {
@@ -1461,7 +1482,7 @@ export class ChatSession {
1461
1482
  // getIdentity()/getHistoryCacheKey() to the new project) can't
1462
1483
  // misattribute this turn's bubbles to that project.
1463
1484
  // (platform === 'none' already returned above, so projectId is the only gate)
1464
- var key = !id.projectId ? '' : id.projectId + '#' + id.platform;
1485
+ var key = chatCacheKey(id.projectId, id.platform, id.userId);
1465
1486
  // True when the pinned chat is NOT the one currently on screen. Then
1466
1487
  // state.messages belongs to a different project and MUST NOT be touched:
1467
1488
  // the turn is staged in the pinned chat's cache instead and shows up when
@@ -2138,7 +2159,8 @@ export class ChatSession {
2138
2159
  if (!group || !group.key) return;
2139
2160
  // The group belongs to the chat on screen, so scope its key the same way
2140
2161
  // _indexKeyOf scopes a queued task's.
2141
- var scoped = this.getHistoryCacheKey() + '|' + group.key;
2162
+ var idn = this.host.getIdentity();
2163
+ var scoped = indexScopeKey(idn.projectId, idn.platform) + '|' + group.key;
2142
2164
  this.cancelledIndexKeys.add(scoped);
2143
2165
  // Remember WHICH RUN was stopped, by the ids of the passes it is made of.
2144
2166
  // Everything else here stops the work without leaving any mark on the
@@ -2688,7 +2710,7 @@ export class ChatSession {
2688
2710
  if (!entry) return '';
2689
2711
  var file = entry.storagePath || entry.filename;
2690
2712
  if (!file) return '';
2691
- return entry.projectId + '#' + entry.platform + '|' + file;
2713
+ return indexScopeKey(entry.projectId, entry.platform) + '|' + file;
2692
2714
  }
2693
2715
 
2694
2716
  /**
@@ -3414,7 +3436,7 @@ export class ChatSession {
3414
3436
  // request is built from. The rescue below compares against this rather
3415
3437
  // than a live getHistoryCacheKey(), so a project switch mid-fetch can't
3416
3438
  // make another chat's in-flight bubbles look local.
3417
- var loadKey = (!id.projectId || id.platform === 'none') ? '' : id.projectId + '#' + id.platform;
3439
+ var loadKey = chatCacheKey(id.projectId, id.platform, id.userId);
3418
3440
  if (token === undefined) token = this.state.gateRefreshToken;
3419
3441
  if ((this.state.loadingHistory && this.state.historyRequestToken === token) || id.platform === 'none' || !id.projectId) {
3420
3442
  return Promise.resolve();
@@ -3443,7 +3465,16 @@ export class ChatSession {
3443
3465
  // stubs, tiled to the surface page's time window with module-level
3444
3466
  // state (buffering + retry safety) — see getSplitChatHistory. Returns
3445
3467
  // the exact single-fetch shape; startKeyHistory is bookkeeping only.
3446
- var fetchHistory = function () { return getSplitChatHistory({ service: projectId, owner: owner, platform: platform, userId: id.userId }, options); };
3468
+ var fetchHistory = function () {
3469
+ return getSplitChatHistory({
3470
+ service: projectId, owner: owner, platform: platform, userId: id.userId,
3471
+ // An anonymous visitor's history is scoped server side by
3472
+ // ip + "(" + user_agent + ")", which two devices behind one NAT
3473
+ // share. Read this device's own queue instead. See
3474
+ // scopeSurfaceToQueue.
3475
+ scopeSurfaceToQueue: !!id.anonymous,
3476
+ }, options);
3477
+ };
3447
3478
 
3448
3479
  return Promise.resolve().then(fetchHistory).catch(function (err: any) {
3449
3480
  if (isAuthExpiredError(err) && !isNonRetryableRequestError(err)) return self.host.refreshSession().then(fetchHistory);
@@ -3468,6 +3499,10 @@ export class ChatSession {
3468
3499
  var mapped = mapHistoryListToMessages(list, platform, {
3469
3500
  clearedAt: self.host.getClearedAt(),
3470
3501
  projectId: id.projectId,
3502
+ // So the `_ownerKey` stamped on server history matches loadKey and
3503
+ // the cache key. Without it every mapped bubble carries a
3504
+ // two-segment stamp that no comparison can ever match.
3505
+ userId: id.userId,
3471
3506
  formatIndexingLabel: self.host.formatIndexingLabel,
3472
3507
  }).messages;
3473
3508
  // Re-apply any previously-hydrated compact bodies: a refresh maps
@@ -3761,6 +3796,7 @@ export class ChatSession {
3761
3796
  var m2 = mapHistoryListToMessages(sorted, platform, {
3762
3797
  clearedAt: self.host.getClearedAt(),
3763
3798
  projectId: id.projectId,
3799
+ userId: id.userId,
3764
3800
  formatIndexingLabel: self.host.formatIndexingLabel,
3765
3801
  }).messages;
3766
3802
  self.applyHydratedBodies(m2);
@@ -4071,6 +4107,17 @@ export class ChatSession {
4071
4107
  size: member.file.size,
4072
4108
  })).catch(function () { });
4073
4109
  });
4110
+ // The access group the host actually recorded this file at. Read
4111
+ // AFTER ensureFileIndexRecord so the value here is the one the
4112
+ // "src::" record really carries, never a second guess at it.
4113
+ var accessGroup: 'public' | 'authorized' | 'private' | undefined;
4114
+ preIndex = preIndex.then(function () {
4115
+ if (alreadyIndexing) return;
4116
+ if (typeof self.host.uploadAccessGroup !== 'function') return;
4117
+ return Promise.resolve(self.host.uploadAccessGroup(member.storagePath))
4118
+ .then(function (g) { accessGroup = g || undefined; })
4119
+ .catch(function () { });
4120
+ });
4074
4121
  // Run a client-side attachment parser (e.g. .hwp) if one matches; its
4075
4122
  // output is inlined into the indexing request (falls back to office
4076
4123
  // extraction / web_fetch when no parser matches or it yields nothing).
@@ -4092,6 +4139,7 @@ export class ChatSession {
4092
4139
  attachment: {
4093
4140
  name: member.file.name, storagePath: member.storagePath,
4094
4141
  mime: mime || undefined, size: member.file.size, url: url,
4142
+ accessGroup: accessGroup,
4095
4143
  },
4096
4144
  parsedContent: parsedContent || undefined,
4097
4145
  }).then(function (ack: any) {
package/src/widget.css CHANGED
@@ -1013,6 +1013,39 @@
1013
1013
  }
1014
1014
  .bq-overwrite-applyall input { cursor: pointer; margin: 0; }
1015
1015
 
1016
+ /* Access-group chooser (who may read a file once it is indexed). A radio list
1017
+ rather than a row of buttons: the three answers are not equally weighted and
1018
+ the hint under each is what actually distinguishes them. */
1019
+ .bq-access-options {
1020
+ display: flex;
1021
+ flex-direction: column;
1022
+ gap: 0.15rem;
1023
+ margin: 0 0 1rem;
1024
+ }
1025
+ .bq-access-option {
1026
+ display: grid;
1027
+ grid-template-columns: auto 1fr;
1028
+ align-items: baseline;
1029
+ column-gap: 0.5rem;
1030
+ padding: 0.5rem 0.6rem;
1031
+ border: 1px solid transparent;
1032
+ cursor: pointer;
1033
+ user-select: none;
1034
+ }
1035
+ .bq-access-option:hover { background: var(--bq-hover-bg); }
1036
+ .bq-access-option input { cursor: pointer; margin: 0; }
1037
+ .bq-access-option-label {
1038
+ font-size: 0.88rem;
1039
+ color: var(--bq-ink);
1040
+ }
1041
+ .bq-access-option-hint {
1042
+ grid-column: 2;
1043
+ font-size: 0.76rem;
1044
+ line-height: 1.35;
1045
+ color: var(--bq-muted);
1046
+ margin-top: 0.15rem;
1047
+ }
1048
+
1016
1049
  /* ============================================================================
1017
1050
  * RESPONSIVE
1018
1051
  * ==========================================================================*/