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/README.md +38 -39
- package/bunnyquery.css +108 -2
- package/bunnyquery.js +1859 -310
- package/dist/engine.cjs +1503 -188
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +906 -37
- package/dist/engine.d.ts +906 -37
- package/dist/engine.mjs +1480 -189
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +11 -11
- package/src/engine/history.ts +23 -6
- package/src/engine/host.ts +77 -3
- package/src/engine/image_preview.ts +0 -0
- package/src/engine/index.ts +13 -0
- package/src/engine/indexing_groups.ts +323 -6
- package/src/engine/link_markup.ts +124 -0
- package/src/engine/links.ts +159 -26
- package/src/engine/office.ts +25 -8
- package/src/engine/prompts/chat_system_prompt.ts +24 -13
- package/src/engine/prompts/indexing_system_prompt.ts +19 -11
- package/src/engine/prompts/indexing_user_message.ts +32 -22
- package/src/engine/requests.ts +302 -14
- package/src/engine/session.ts +1424 -114
- package/src/engine/viewport_fill.ts +51 -4
- package/styles/chat.css +108 -2
package/package.json
CHANGED
package/src/engine/budget.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token-budgeting (pure). Moved verbatim from the chatbox. Constants are shared
|
|
3
3
|
* module-level values (identical in both consumers); a config knob is premature.
|
|
4
|
-
* buildBoundedChatMessages now takes `
|
|
4
|
+
* buildBoundedChatMessages now takes `projectId` in its options so it can pass it
|
|
5
5
|
* to sanitizeAttachmentLinksForHistory (which used to read a global).
|
|
6
6
|
*/
|
|
7
7
|
import { sanitizeAttachmentLinksForHistory } from './links';
|
|
@@ -52,16 +52,16 @@ export function registerModelContextWindows(models: Array<{ id?: string; max_inp
|
|
|
52
52
|
/** Per-project override, keyed by service id. Set from the project settings. */
|
|
53
53
|
var projectContextWindows: Record<string, number> = {};
|
|
54
54
|
|
|
55
|
-
export function setProjectContextWindow(
|
|
56
|
-
var key = (
|
|
55
|
+
export function setProjectContextWindow(projectId: string, tokens: number | null | undefined): void {
|
|
56
|
+
var key = (projectId || '').trim();
|
|
57
57
|
if (!key) return;
|
|
58
58
|
var n = Number(tokens);
|
|
59
59
|
if (Number.isFinite(n) && n > 0) projectContextWindows[key] = Math.floor(n);
|
|
60
60
|
else delete projectContextWindows[key];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function getProjectContextWindow(
|
|
64
|
-
var key = (
|
|
63
|
+
export function getProjectContextWindow(projectId: string): number | null {
|
|
64
|
+
var key = (projectId || '').trim();
|
|
65
65
|
return key && projectContextWindows[key] ? projectContextWindows[key] : null;
|
|
66
66
|
}
|
|
67
67
|
export var OUTPUT_TOKEN_RESERVE = 22000;
|
|
@@ -99,8 +99,8 @@ export function estimateMessageTokens(msg: { role: string; content: string }): n
|
|
|
99
99
|
* such as 'claude-opus-4-7-20260101' resolves via 'claude-opus-4-7'. The walk
|
|
100
100
|
* stops at the first hit, so a more specific entry always wins over its family.
|
|
101
101
|
*/
|
|
102
|
-
export function getContextWindow(platform: string, model?: string,
|
|
103
|
-
var override =
|
|
102
|
+
export function getContextWindow(platform: string, model?: string, projectId?: string): number {
|
|
103
|
+
var override = projectId ? getProjectContextWindow(projectId) : null;
|
|
104
104
|
if (override) return override;
|
|
105
105
|
|
|
106
106
|
var normalized = (model || '').trim().toLowerCase();
|
|
@@ -128,11 +128,11 @@ export type BoundedChatOptions = {
|
|
|
128
128
|
systemPrompt: string;
|
|
129
129
|
history: Array<{ role: string; content: string }>;
|
|
130
130
|
/** Used to strip/rewrite expired attachment links in older user turns. */
|
|
131
|
-
|
|
131
|
+
projectId: string;
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
export function buildBoundedChatMessages(options: BoundedChatOptions) {
|
|
135
|
-
var contextWindow = getContextWindow(options.platform, options.model, options.
|
|
135
|
+
var contextWindow = getContextWindow(options.platform, options.model, options.projectId);
|
|
136
136
|
var contextBasedBudget = Math.max(MIN_INPUT_TOKEN_BUDGET,
|
|
137
137
|
contextWindow - OUTPUT_TOKEN_RESERVE - TOOL_AND_RESPONSE_BUFFER);
|
|
138
138
|
// Scaling is gated on an EXPLICIT per-project window set from project
|
|
@@ -143,7 +143,7 @@ export function buildBoundedChatMessages(options: BoundedChatOptions) {
|
|
|
143
143
|
// sends more history instead of being absorbed by a hardcoded ceiling.
|
|
144
144
|
// Both derive from contextBasedBudget (pre-Claude-cap) so the two platforms
|
|
145
145
|
// scale symmetrically rather than the Claude cap compounding the ratio down.
|
|
146
|
-
var scaled = !!(options.
|
|
146
|
+
var scaled = !!(options.projectId && getProjectContextWindow(options.projectId));
|
|
147
147
|
var claudeInputCap = scaled
|
|
148
148
|
? Math.max(CLAUDE_PER_REQUEST_INPUT_CAP, Math.round(contextBasedBudget * CLAUDE_INPUT_CAP_RATIO))
|
|
149
149
|
: CLAUDE_PER_REQUEST_INPUT_CAP;
|
|
@@ -167,7 +167,7 @@ export function buildBoundedChatMessages(options: BoundedChatOptions) {
|
|
|
167
167
|
// Sanitize BOTH roles: user turns via the "Attached files:" block, assistant
|
|
168
168
|
// turns via the safe db-only path (forAssistant=true) so a volatile db url the
|
|
169
169
|
// model emitted doesn't get replayed into the LLM context as a dead link.
|
|
170
|
-
var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.
|
|
170
|
+
var sanitized = sanitizeAttachmentLinksForHistory(stripped, options.projectId, m.role !== 'user');
|
|
171
171
|
return Object.assign({}, m, { content: sanitized });
|
|
172
172
|
});
|
|
173
173
|
var bounded: Array<{ role: string; content: string }> = [], used = 0;
|
package/src/engine/history.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* History mapping (pure). Moved verbatim from the chatbox. The clear-horizon
|
|
3
3
|
* timestamp and the "Indexing: …" display label are INJECTED (clearedAt param,
|
|
4
4
|
* formatIndexingLabel callback) so the engine touches neither localStorage nor
|
|
5
|
-
* view-specific display formatting.
|
|
5
|
+
* view-specific display formatting. projectId is passed for link sanitization.
|
|
6
6
|
*/
|
|
7
|
-
import { extractClaudeText, extractOpenAIText } from './requests';
|
|
7
|
+
import { extractClaudeText, extractOpenAIText, INDEXING_COMPLETE_MARKER, EMPTY_INDEXING_REPLY } from './requests';
|
|
8
8
|
import { isErrorResponseBody, getErrorMessage } from './errors';
|
|
9
9
|
import { sanitizeAttachmentLinksForHistory } from './links';
|
|
10
10
|
|
|
@@ -82,7 +82,7 @@ export function parseIndexingRequestText(userText: any): IndexingRequestRef | nu
|
|
|
82
82
|
|
|
83
83
|
export type MapHistoryOptions = {
|
|
84
84
|
clearedAt: number;
|
|
85
|
-
|
|
85
|
+
projectId: string;
|
|
86
86
|
/** View-side display formatter for "Indexing:/Reindexing: …" bubbles. */
|
|
87
87
|
formatIndexingLabel: (name: string, mime?: string, size?: number | null, storagePath?: string, reindex?: boolean, continued?: boolean) => string;
|
|
88
88
|
};
|
|
@@ -103,6 +103,18 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
|
|
|
103
103
|
var userText = extractLastUserTextFromRequest(requestBody);
|
|
104
104
|
var assistantText = isPending ? '' : ((extractAssistantText(response) || '').trim() || '');
|
|
105
105
|
var isErrorResponse = !isPending && (isFailed || isErrorResponseBody(response));
|
|
106
|
+
// Record the completion marker, then STRIP it — both, and in that order.
|
|
107
|
+
// Recording gives the display layer a structured signal instead of a substring
|
|
108
|
+
// search over model prose. Stripping matches the live resolution path: without
|
|
109
|
+
// it the literal token rendered inside the bubble on every history load, which
|
|
110
|
+
// is not a post-reload curiosity — a first-page load REPLACES the live bubbles,
|
|
111
|
+
// and those responses are now on screen far more often than they used to be.
|
|
112
|
+
//
|
|
113
|
+
// Gated on _isBgTask: only an INDEXING pass has a protocol token to hide. An
|
|
114
|
+
// ordinary reply that merely mentions it keeps its own words.
|
|
115
|
+
var reportedComplete = !!(item && item._isBgTask) && !isErrorResponse && !!assistantText &&
|
|
116
|
+
assistantText.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
|
|
117
|
+
if (reportedComplete) assistantText = assistantText.split(INDEXING_COMPLETE_MARKER).join('').trim();
|
|
106
118
|
var serverItemId = item && typeof item.id === 'string' && item.id ? item.id : undefined;
|
|
107
119
|
// A USER bubble shows when the request was made (`created`); an ASSISTANT
|
|
108
120
|
// bubble shows when its response landed (`updated`). Fall back to the other
|
|
@@ -137,7 +149,7 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
|
|
|
137
149
|
displayContent = userText;
|
|
138
150
|
}
|
|
139
151
|
} else {
|
|
140
|
-
displayContent = sanitizeAttachmentLinksForHistory(userText, opts.
|
|
152
|
+
displayContent = sanitizeAttachmentLinksForHistory(userText, opts.projectId);
|
|
141
153
|
}
|
|
142
154
|
var userMsg: any = { role: 'user', content: displayContent };
|
|
143
155
|
if (isInProcess) userMsg.isPendingInProcess = true;
|
|
@@ -163,13 +175,18 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
|
|
|
163
175
|
if (serverItemId !== undefined) em._serverItemId = serverItemId;
|
|
164
176
|
if (replyTs !== undefined) em._ts = replyTs;
|
|
165
177
|
mapped.push(em);
|
|
166
|
-
|
|
178
|
+
// `|| reportedComplete`: a pass whose ENTIRE answer was the completion token
|
|
179
|
+
// strips down to an empty string, and the plain `assistantText` guard then
|
|
180
|
+
// emitted no bubble at all — while the live path emitted one. The run read as
|
|
181
|
+
// finished live and unfinished after a reload, so the row's loader came back.
|
|
182
|
+
} else if (assistantText || reportedComplete) {
|
|
167
183
|
// Safe db-only sanitize (forAssistant) so a volatile db url the model
|
|
168
184
|
// emitted renders as a re-mintable `_expired_.url` link, not a dead one.
|
|
169
|
-
var okm: any = { role: 'assistant', content: sanitizeAttachmentLinksForHistory(assistantText, opts.
|
|
185
|
+
var okm: any = { role: 'assistant', content: sanitizeAttachmentLinksForHistory(assistantText, opts.projectId, true) || EMPTY_INDEXING_REPLY };
|
|
170
186
|
if (item._isBgTask) okm.isBackgroundTask = true;
|
|
171
187
|
if (serverItemId !== undefined) okm._serverItemId = serverItemId;
|
|
172
188
|
if (replyTs !== undefined) okm._ts = replyTs;
|
|
189
|
+
if (reportedComplete) okm._indexComplete = true;
|
|
173
190
|
mapped.push(okm);
|
|
174
191
|
}
|
|
175
192
|
});
|
package/src/engine/host.ts
CHANGED
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export interface ChatIdentity {
|
|
11
|
-
|
|
11
|
+
projectId: string;
|
|
12
|
+
/**
|
|
13
|
+
* The PUBLIC project ID: the formatted two-segment token (skapi.project_id).
|
|
14
|
+
* projectId above is the RAW regional code the wire endpoints take; the public
|
|
15
|
+
* token is what MCP tools accept and what prompts must show the model, since the
|
|
16
|
+
* model copies it verbatim into tool calls. Optional for older hosts; prompts
|
|
17
|
+
* fall back to the raw code when absent.
|
|
18
|
+
*/
|
|
19
|
+
publicProjectId?: string;
|
|
12
20
|
owner: string;
|
|
13
|
-
/** Per-user queue name (falls back to
|
|
21
|
+
/** Per-user queue name (falls back to projectId). */
|
|
14
22
|
userId: string;
|
|
15
23
|
platform: 'claude' | 'openai' | 'none';
|
|
16
24
|
model?: string;
|
|
@@ -28,6 +36,11 @@ export interface ChatIdentity {
|
|
|
28
36
|
export interface PinnedDispatchContext {
|
|
29
37
|
identity: ChatIdentity;
|
|
30
38
|
systemPrompt: string;
|
|
39
|
+
/** Id returned by stageOutgoingMessage. The turn's bubble is already on
|
|
40
|
+
* screen (staged while its attachments upload), so dispatchComposedMessage
|
|
41
|
+
* REPLACES that bubble in place instead of pushing a second one at the
|
|
42
|
+
* bottom — the message keeps the position it was sent in. */
|
|
43
|
+
stageId?: string;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -56,13 +69,50 @@ export interface ChatMessage {
|
|
|
56
69
|
isPendingInProcess?: boolean;
|
|
57
70
|
isPendingQueued?: boolean;
|
|
58
71
|
isPendingOlder?: boolean;
|
|
72
|
+
/** PROTOCOL flag: true from the moment a queued turn is dispatched until the
|
|
73
|
+
* server acknowledges it. It is the token the ack's findIndex matches on, so
|
|
74
|
+
* nothing may clear it early. It is NOT a style input — see _dimSending. */
|
|
59
75
|
isSendingToServer?: boolean;
|
|
76
|
+
/** PRESENTATIONAL flag: render this bubble dimmed because the turn has not been
|
|
77
|
+
* handed over yet. Split from isSendingToServer because an ATTACHMENT turn is
|
|
78
|
+
* un-dimmed the instant its files finish indexing, while the request itself is
|
|
79
|
+
* still un-acked for another moment; dropping isSendingToServer to achieve that
|
|
80
|
+
* would cost the turn its _serverItemId (the ack matches on that flag alone, and
|
|
81
|
+
* a _useBgQueue turn is excluded from every fallback that would recover it). */
|
|
82
|
+
_dimSending?: boolean;
|
|
60
83
|
isCancelled?: boolean;
|
|
61
84
|
isError?: boolean;
|
|
62
85
|
isBackgroundTask?: boolean;
|
|
63
86
|
/** Set on background-indexing REQUEST bubbles only (see IndexingFileRef). */
|
|
64
87
|
_indexFile?: IndexingFileRef;
|
|
88
|
+
/** Set on a background-indexing RESPONSE bubble whose raw answer carried the
|
|
89
|
+
* INDEXING_COMPLETE marker. Stamped before the marker is stripped for display,
|
|
90
|
+
* in every path that builds one (live resolution and both history mappers), so
|
|
91
|
+
* a run reads the same before and after a reload.
|
|
92
|
+
*
|
|
93
|
+
* Meaningful ONLY for a client-driven chain, where it is the very signal
|
|
94
|
+
* maybeResumeIndexing stops on. The worker-driven paths (PDF vision, windowed
|
|
95
|
+
* reads) advance off the renderer's page count and their prompt deliberately
|
|
96
|
+
* never asks for the marker, so a model that emits one there is guessing —
|
|
97
|
+
* which is how an 88-page file once "finished" at page 15. */
|
|
98
|
+
_indexComplete?: boolean;
|
|
65
99
|
_useBgQueue?: boolean;
|
|
100
|
+
/** Local id of a turn STAGED at Send time while its attachments upload. The
|
|
101
|
+
* bubble exists before any server request does, so it is never matched by
|
|
102
|
+
* _serverItemId and is never promoted/cancelled by the queue machinery —
|
|
103
|
+
* dispatchComposedMessage consumes it (pinned.stageId) when the turn is
|
|
104
|
+
* finally sent. Staged bubbles are deliberately kept OUT of the history
|
|
105
|
+
* cache: an unmount kills the upload that would resolve them, so a cached
|
|
106
|
+
* copy would replay as a bubble that uploads forever. */
|
|
107
|
+
_stageId?: string;
|
|
108
|
+
/** Staged-turn phase 1: its files are still uploading. Renders
|
|
109
|
+
* "(Uploading files...)", dimmed. */
|
|
110
|
+
isUploadingAttachments?: boolean;
|
|
111
|
+
/** Staged-turn phase 2: the files are up and the turn is waiting for the whole
|
|
112
|
+
* background-indexing chain behind them to finish. Renders "(Indexing files...)",
|
|
113
|
+
* still dimmed. Cleared (with _dimSending) by markStagedMessageReady the moment
|
|
114
|
+
* the queue drains, which is when the turn genuinely becomes "(In queue)". */
|
|
115
|
+
isAwaitingIndexing?: boolean;
|
|
66
116
|
_serverItemId?: string;
|
|
67
117
|
_localId?: string;
|
|
68
118
|
_cancelling?: boolean;
|
|
@@ -73,7 +123,7 @@ export interface ChatMessage {
|
|
|
73
123
|
* it is created, then reconciled to the server value on the next history load.
|
|
74
124
|
* Absent while a turn is still pending, so no time shows on a "Thinking" bubble. */
|
|
75
125
|
_ts?: number;
|
|
76
|
-
// History cache key (`
|
|
126
|
+
// History cache key (`projectId#platform`) this bubble was created under.
|
|
77
127
|
// Stamped on LOCALLY-created bubbles only (the optimistic user message and
|
|
78
128
|
// its "Thinking..." placeholder); server-mapped bubbles are identified by
|
|
79
129
|
// _serverItemId instead. The dashboard renders every project through ONE
|
|
@@ -97,6 +147,24 @@ export interface ChatState {
|
|
|
97
147
|
historyStartKeyHistory: string[];
|
|
98
148
|
historyRequestToken: number;
|
|
99
149
|
gateRefreshToken: number;
|
|
150
|
+
/** Files the SERVER still has unresolved indexing work for, by the key a
|
|
151
|
+
* collapsed row uses (storage path, else filename). Lives on the state rather
|
|
152
|
+
* than privately so a reactive consumer re-renders when it changes. */
|
|
153
|
+
liveIndexKeys: { [fileKey: string]: boolean };
|
|
154
|
+
/** Whether `liveIndexKeys` has been answered at least once for this chat. False
|
|
155
|
+
* means "we have not found out", which the display layer reads as still
|
|
156
|
+
* working — never as an all-clear. */
|
|
157
|
+
liveIndexChecked: boolean;
|
|
158
|
+
/** Server item ids of the indexing passes that existed — on the row, or on the
|
|
159
|
+
* bg queue — when the user STOPPED that file. Two readers, one fact:
|
|
160
|
+
* buildChatDisplayList reports the run as stopped when it holds any of them
|
|
161
|
+
* (a stop routinely leaves no other trace), and _applyIndexCancellations
|
|
162
|
+
* refuses to let one of them lift the stop the way a genuinely new indexing
|
|
163
|
+
* request does. Ids, not file keys: they name the RUN that was stopped, so a
|
|
164
|
+
* later re-index of the same file cannot inherit it. On the state, like
|
|
165
|
+
* liveIndexKeys, so a reactive consumer re-renders the moment a stop is
|
|
166
|
+
* recorded — a stop with nothing left to cancel changes no message at all. */
|
|
167
|
+
stoppedIndexIds: { [serverItemId: string]: boolean };
|
|
100
168
|
}
|
|
101
169
|
|
|
102
170
|
export interface ChatHost {
|
|
@@ -155,6 +223,12 @@ export interface ChatHost {
|
|
|
155
223
|
* through to a plain re-index. Implementations must be best-effort (swallow
|
|
156
224
|
* "not found" / permission errors so indexing still proceeds). */
|
|
157
225
|
deleteExistingFileRecord?(storagePath: string): Promise<any>;
|
|
226
|
+
/**
|
|
227
|
+
* Create the file's "src::<storagePath>" record before indexing starts, so every pass has a
|
|
228
|
+
* reference target that exists. Optional: a host without it keeps the old behaviour, where
|
|
229
|
+
* whichever pass got there first created the record and the others hoped it had.
|
|
230
|
+
*/
|
|
231
|
+
ensureFileIndexRecord?(storagePath: string, meta?: { name?: string; mime?: string; size?: number }): Promise<any>;
|
|
158
232
|
/** Map a relative path to the consumer's db storage key (e.g. uid-prefixed). */
|
|
159
233
|
storagePathFor(relPath: string): string;
|
|
160
234
|
getMimeType(name: string): string | null;
|
|
Binary file
|
package/src/engine/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export * from './budget';
|
|
|
49
49
|
// block and a server-published file open identically in Excel, Word and a browser.
|
|
50
50
|
export * from './download_encoding';
|
|
51
51
|
export * from './links';
|
|
52
|
+
export * from './link_markup';
|
|
53
|
+
export * from './image_preview';
|
|
52
54
|
export * from './time';
|
|
53
55
|
export * from './ai_agent';
|
|
54
56
|
export {
|
|
@@ -97,7 +99,11 @@ export {
|
|
|
97
99
|
export {
|
|
98
100
|
// constants
|
|
99
101
|
POLL_INTERVAL,
|
|
102
|
+
MAX_CONCURRENT_BG_POLLS,
|
|
103
|
+
getVisionProfile,
|
|
104
|
+
type VisionProfile,
|
|
100
105
|
BG_INDEXING_QUEUE_SUFFIX,
|
|
106
|
+
bgIndexingQueueName,
|
|
101
107
|
isBgIndexingQueue,
|
|
102
108
|
MCP_NAME,
|
|
103
109
|
DEFAULT_CLAUDE_MODEL,
|
|
@@ -116,6 +122,13 @@ export {
|
|
|
116
122
|
// content transforms
|
|
117
123
|
transformContentWithImages,
|
|
118
124
|
transformContentWithOpenAIImages,
|
|
125
|
+
// The token an indexing pass ends on when it has read the whole file. Exported
|
|
126
|
+
// because agent.vue's FORKED history mapper has to record-then-strip it exactly
|
|
127
|
+
// as the engine's own mapper does, or a run reads differently in the two clients.
|
|
128
|
+
INDEXING_COMPLETE_MARKER,
|
|
129
|
+
// Stand-in text for a pass whose whole answer was that token; both mappers need
|
|
130
|
+
// it so a run reads the same live and after a reload.
|
|
131
|
+
EMPTY_INDEXING_REPLY,
|
|
119
132
|
// types
|
|
120
133
|
type ClaudeRole,
|
|
121
134
|
type ClaudeMessage,
|