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.
- package/bunnyquery.css +33 -0
- package/bunnyquery.js +331 -41
- package/dist/engine.cjs +107 -27
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +147 -3
- package/dist/engine.d.ts +147 -3
- package/dist/engine.mjs +105 -28
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/history.ts +111 -5
- package/src/engine/host.ts +29 -0
- package/src/engine/index.ts +5 -0
- package/src/engine/prompts/chat_system_prompt.ts +20 -1
- package/src/engine/prompts/index.ts +1 -0
- package/src/engine/prompts/indexing_system_prompt.ts +13 -1
- package/src/engine/prompts/indexing_user_message.ts +29 -2
- package/src/engine/requests.ts +57 -6
- package/src/engine/session.ts +59 -11
- package/src/widget.css +33 -0
package/src/engine/session.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 = (
|
|
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 () {
|
|
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
|
* ==========================================================================*/
|