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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -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 `serviceId` in its options so it can pass it
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(serviceId: string, tokens: number | null | undefined): void {
56
- var key = (serviceId || '').trim();
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(serviceId: string): number | null {
64
- var key = (serviceId || '').trim();
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, serviceId?: string): number {
103
- var override = serviceId ? getProjectContextWindow(serviceId) : null;
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
- serviceId: string;
131
+ projectId: string;
132
132
  };
133
133
 
134
134
  export function buildBoundedChatMessages(options: BoundedChatOptions) {
135
- var contextWindow = getContextWindow(options.platform, options.model, options.serviceId);
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.serviceId && getProjectContextWindow(options.serviceId));
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.serviceId, m.role !== 'user');
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;
@@ -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. serviceId is passed for link sanitization.
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
- serviceId: string;
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.serviceId);
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
- } else if (assistantText) {
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.serviceId, true) };
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
  });
@@ -8,9 +8,17 @@
8
8
  */
9
9
 
10
10
  export interface ChatIdentity {
11
- serviceId: string;
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 serviceId). */
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 (`serviceId#platform`) this bubble was created under.
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
@@ -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,