bunnyquery 1.11.1 → 1.13.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/README.md +5 -0
- package/bunnyquery.css +59 -0
- package/bunnyquery.js +332 -9
- package/dist/engine.cjs +263 -8
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +93 -1
- package/dist/engine.d.ts +93 -1
- package/dist/engine.mjs +263 -9
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/errors.ts +15 -0
- package/src/engine/history.ts +15 -2
- package/src/engine/host.ts +4 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/indexing_groups.ts +24 -3
- package/src/engine/prompts/chat_system_prompt.ts +23 -0
- package/src/engine/session.ts +277 -6
- package/styles/chat.css +59 -0
package/dist/engine.cjs
CHANGED
|
@@ -359,10 +359,15 @@ function buildChatSystemPrompt(params) {
|
|
|
359
359
|
const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
|
|
360
360
|
const g = params.indexAccessGroup;
|
|
361
361
|
const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
|
|
362
|
+
const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
|
|
363
|
+
WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
|
|
364
|
+
KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
|
|
365
|
+
These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
|
|
362
366
|
let systemPrompt = `
|
|
363
367
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
364
368
|
Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
|
|
365
369
|
Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
|
|
370
|
+
${groundingRules}
|
|
366
371
|
NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
|
|
367
372
|
CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
|
|
368
373
|
A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
|
|
@@ -627,6 +632,11 @@ function getErrorMessage(input) {
|
|
|
627
632
|
}
|
|
628
633
|
return "Something went wrong.";
|
|
629
634
|
}
|
|
635
|
+
function isRetriedFailure(input) {
|
|
636
|
+
var envErr = csrEnvelopeError(input);
|
|
637
|
+
if (envErr !== void 0) input = envErr;
|
|
638
|
+
return !!input && typeof input === "object" && input.retried === true;
|
|
639
|
+
}
|
|
630
640
|
function isErrorResponseBody(response) {
|
|
631
641
|
var envErr = csrEnvelopeError(response);
|
|
632
642
|
if (envErr !== void 0) response = envErr;
|
|
@@ -3250,8 +3260,9 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
3250
3260
|
var createdTs = Number(item && item.created);
|
|
3251
3261
|
var updatedTs = Number(item && item.updated);
|
|
3252
3262
|
var executedTs = Number(item && item.executed);
|
|
3263
|
+
var respondedTs = Number(item && item.responded);
|
|
3253
3264
|
var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : void 0;
|
|
3254
|
-
var replyTs = isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
|
|
3265
|
+
var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs : isFinite(updatedTs) && updatedTs > 0 ? updatedTs : isFinite(createdTs) && createdTs > 0 ? createdTs : void 0;
|
|
3255
3266
|
if (userText) {
|
|
3256
3267
|
var displayContent;
|
|
3257
3268
|
var indexFile = void 0;
|
|
@@ -3298,6 +3309,7 @@ function mapHistoryListToMessages(list, platform, opts) {
|
|
|
3298
3309
|
mapped.push(ph);
|
|
3299
3310
|
} else if (isQueued) ; else if (isErrorResponse) {
|
|
3300
3311
|
var em = { role: "assistant", content: getErrorMessage(response), isError: true };
|
|
3312
|
+
if (isRetriedFailure(response)) em.isRetried = true;
|
|
3301
3313
|
if (item._fromBgChain) em._fromBgChain = true;
|
|
3302
3314
|
if (item._isBgTask) em.isBackgroundTask = true;
|
|
3303
3315
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
@@ -3877,6 +3889,13 @@ var ChatSession = class {
|
|
|
3877
3889
|
// the request budget MAX_CONCURRENT_BG_POLLS exists to protect.
|
|
3878
3890
|
/** Live streams by server item id. One per in-flight streamed turn. */
|
|
3879
3891
|
this.liveStreams = {};
|
|
3892
|
+
/** When skapi recorded a turn's response as having finished arriving, by server
|
|
3893
|
+
* item id. Read off the terminal envelope of a STREAMED turn (the only settle
|
|
3894
|
+
* that carries it, see _settleLiveStream) and used as the answer bubble's time
|
|
3895
|
+
* in place of this browser's clock. Entries are never removed: a settle reaches
|
|
3896
|
+
* here twice (the poll's onResponse, then the promise), and the value for an id
|
|
3897
|
+
* cannot change. */
|
|
3898
|
+
this._respondedAt = {};
|
|
3880
3899
|
// ─── compact-stub hydration ─────────────────────────────────────────────
|
|
3881
3900
|
// Split-fetch bg pages arrive as label stubs (no bodies). When the user
|
|
3882
3901
|
// expands a row, the real reply text is fetched per item (csr-poll point
|
|
@@ -4326,7 +4345,9 @@ var ChatSession = class {
|
|
|
4326
4345
|
timers = [];
|
|
4327
4346
|
};
|
|
4328
4347
|
var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
|
|
4348
|
+
var resolveRaced = null;
|
|
4329
4349
|
var raced = new Promise(function(resolve, reject) {
|
|
4350
|
+
resolveRaced = resolve;
|
|
4330
4351
|
base.then(function(res) {
|
|
4331
4352
|
if (settled) return;
|
|
4332
4353
|
settled = true;
|
|
@@ -4367,9 +4388,16 @@ var ChatSession = class {
|
|
|
4367
4388
|
});
|
|
4368
4389
|
});
|
|
4369
4390
|
raced.stop = function() {
|
|
4391
|
+
if (settled) return;
|
|
4370
4392
|
settled = true;
|
|
4371
4393
|
clearProbes();
|
|
4372
|
-
if (stopBase)
|
|
4394
|
+
if (stopBase) {
|
|
4395
|
+
try {
|
|
4396
|
+
stopBase();
|
|
4397
|
+
} catch (e) {
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
if (resolveRaced) resolveRaced({ id: itemId, status: "stopped" });
|
|
4373
4401
|
};
|
|
4374
4402
|
return raced;
|
|
4375
4403
|
}
|
|
@@ -4393,6 +4421,23 @@ var ChatSession = class {
|
|
|
4393
4421
|
});
|
|
4394
4422
|
return n;
|
|
4395
4423
|
}
|
|
4424
|
+
_noteRespondedAt(itemId, response) {
|
|
4425
|
+
if (!itemId || !isCsrStatusEnvelope(response)) return;
|
|
4426
|
+
var at = Number(response.responded);
|
|
4427
|
+
if (!isFinite(at) || at <= 0) return;
|
|
4428
|
+
if (!this._respondedAt) this._respondedAt = {};
|
|
4429
|
+
this._respondedAt[itemId] = at;
|
|
4430
|
+
}
|
|
4431
|
+
/** The time a live answer bubble is stamped with: when skapi received the
|
|
4432
|
+
* response, if the settle told us, else now. "Now" is what every bubble got
|
|
4433
|
+
* before, and it is still right for a buffered turn, whose settle hands back the
|
|
4434
|
+
* destination's body and nothing of skapi's; a history load then replaces it
|
|
4435
|
+
* with the server's value. */
|
|
4436
|
+
_replyStampFor(msg) {
|
|
4437
|
+
var id = msg ? msg._serverItemId : void 0;
|
|
4438
|
+
var at = typeof id === "string" && id && this._respondedAt ? this._respondedAt[id] : void 0;
|
|
4439
|
+
return typeof at === "number" ? at : wallClockNow();
|
|
4440
|
+
}
|
|
4396
4441
|
/**
|
|
4397
4442
|
* Open (or re-open) the live stream for `itemId`, or null when this poll must
|
|
4398
4443
|
* not carry one.
|
|
@@ -4582,6 +4627,7 @@ var ChatSession = class {
|
|
|
4582
4627
|
_settleLiveStream(st, response) {
|
|
4583
4628
|
this._closeLiveStream(st, true);
|
|
4584
4629
|
if (!isCsrStatusEnvelope(response)) return response;
|
|
4630
|
+
this._noteRespondedAt(st.id, response);
|
|
4585
4631
|
if (response.status !== "resolved") return response;
|
|
4586
4632
|
if (!this._mayFinalize(st)) this._rec().incomplete[st.id] = true;
|
|
4587
4633
|
if (st.finalBody == null) return response;
|
|
@@ -5382,6 +5428,7 @@ var ChatSession = class {
|
|
|
5382
5428
|
}
|
|
5383
5429
|
return response;
|
|
5384
5430
|
}).then(function(response) {
|
|
5431
|
+
if (isPollStopped(response) || self._isCancelledPollResult(response)) return { content: "", isError: false, cancelled: true };
|
|
5385
5432
|
if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
|
|
5386
5433
|
var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
|
|
5387
5434
|
answer = (answer || "").trim();
|
|
@@ -5391,6 +5438,10 @@ var ChatSession = class {
|
|
|
5391
5438
|
}).then(function(result) {
|
|
5392
5439
|
delete self.pendingAgentRequests[params.key];
|
|
5393
5440
|
if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
|
|
5441
|
+
if (result.cancelled) {
|
|
5442
|
+
if (dispatchItemId) self._settleCancelledTurn(dispatchItemId, params.key);
|
|
5443
|
+
return result;
|
|
5444
|
+
}
|
|
5394
5445
|
var reply = { role: "assistant", content: result.content, isError: result.isError };
|
|
5395
5446
|
if (dispatchItemId) reply._serverItemId = dispatchItemId;
|
|
5396
5447
|
self._applyReplyToCache(params.key, reply, dispatchItemId);
|
|
@@ -5869,8 +5920,9 @@ var ChatSession = class {
|
|
|
5869
5920
|
}
|
|
5870
5921
|
});
|
|
5871
5922
|
Promise.resolve(run).catch(function() {
|
|
5872
|
-
}).then(function() {
|
|
5923
|
+
}).then(function(result) {
|
|
5873
5924
|
self.state.sending = false;
|
|
5925
|
+
if (result && result.cancelled) return;
|
|
5874
5926
|
if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
|
|
5875
5927
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
5876
5928
|
self.host.scrollToBottomIfSticky(true);
|
|
@@ -6017,7 +6069,7 @@ var ChatSession = class {
|
|
|
6017
6069
|
return thinkingIdx !== -1 ? thinkingIdx : userIdx >= 0 ? userIdx + 1 : -1;
|
|
6018
6070
|
}
|
|
6019
6071
|
insertAtTarget(msg, targetIdx) {
|
|
6020
|
-
if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts =
|
|
6072
|
+
if (msg && msg.role === "assistant" && msg._ts === void 0) msg._ts = this._replyStampFor(msg);
|
|
6021
6073
|
var tgt = targetIdx >= 0 ? this.state.messages[targetIdx] : void 0;
|
|
6022
6074
|
var replaceable = !!tgt && !!tgt.isPending && !tgt.isBackgroundTask && this._isOwnPlaceholderOf(targetIdx, this._owningUserIndex(targetIdx));
|
|
6023
6075
|
if (replaceable) this.state.messages[targetIdx] = msg;
|
|
@@ -6049,6 +6101,10 @@ var ChatSession = class {
|
|
|
6049
6101
|
}
|
|
6050
6102
|
onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
|
|
6051
6103
|
if (serverId) this.historyItemPolls.delete(serverId);
|
|
6104
|
+
if (this._isCancelledPollResult(response)) {
|
|
6105
|
+
if (serverId) this._settleCancelledTurn(serverId, ownerKey);
|
|
6106
|
+
return;
|
|
6107
|
+
}
|
|
6052
6108
|
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
6053
6109
|
var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
|
|
6054
6110
|
this._applyReplyToCache(ownerKey, offReply, serverId);
|
|
@@ -6248,6 +6304,191 @@ var ChatSession = class {
|
|
|
6248
6304
|
}
|
|
6249
6305
|
});
|
|
6250
6306
|
}
|
|
6307
|
+
/**
|
|
6308
|
+
* Stop waiting for the reply to the chat turn that is running now, and move on.
|
|
6309
|
+
*
|
|
6310
|
+
* `msg` is the turn's pending ASSISTANT bubble (the dot-trail "Thinking"
|
|
6311
|
+
* placeholder), which is where both views draw the control. A turn's two bubbles
|
|
6312
|
+
* carry the same _serverItemId, so it is also how the request is found.
|
|
6313
|
+
* cancelQueuedMessage is the sibling for a turn still waiting in the queue and
|
|
6314
|
+
* takes the USER bubble; the two differ in what they are handed and in what has
|
|
6315
|
+
* to be unwound afterwards, not in what the server is asked.
|
|
6316
|
+
*
|
|
6317
|
+
* What ends the wait:
|
|
6318
|
+
* 1. csr-cancel marks the running row cancelled. The worker stops relaying and
|
|
6319
|
+
* never stores an answer for it, so the reply cannot turn up later on this
|
|
6320
|
+
* device or another, and history replays the turn as cancelled;
|
|
6321
|
+
* 2. the poll on it is stopped, which settles whichever chain was awaiting it
|
|
6322
|
+
* with a stopped result: dispatchAgentRequest's for an immediate send, the
|
|
6323
|
+
* queued chain for a promoted turn, the history poll after a reload. Each
|
|
6324
|
+
* reads a stopped or cancelled result as "no reply" and releases what it
|
|
6325
|
+
* holds (state.sending, pendingAgentRequests, historyItemPolls), which is
|
|
6326
|
+
* what lets the next send go out immediately instead of queueing;
|
|
6327
|
+
* 3. the bubbles settle at once, without waiting for that chain: the user
|
|
6328
|
+
* bubble takes its cancelled form, the placeholder goes, and the next queued
|
|
6329
|
+
* turn, if there is one, is promoted so its own "Thinking" appears.
|
|
6330
|
+
*
|
|
6331
|
+
* What it does NOT do: the provider call already in flight on the worker runs
|
|
6332
|
+
* to its end (a streamed one stops within about a second). A message sent
|
|
6333
|
+
* behind it on the same queue starts once the worker acks the cancelled row.
|
|
6334
|
+
*
|
|
6335
|
+
* Refused without a server id (the ack has not come back yet; the views disable
|
|
6336
|
+
* the control until it has) and while a cancel is already out. Indexing passes
|
|
6337
|
+
* are not this method's business: their Stop is cancelIndexingGroup, which ends
|
|
6338
|
+
* the file's whole chain.
|
|
6339
|
+
*/
|
|
6340
|
+
cancelPendingReply(msg, idx) {
|
|
6341
|
+
var self = this;
|
|
6342
|
+
var id = this.host.getIdentity();
|
|
6343
|
+
var serverId = msg._serverItemId;
|
|
6344
|
+
if (!serverId || msg._cancelling) return;
|
|
6345
|
+
if (msg.role !== "assistant" || !msg.isPending || msg.isBackgroundTask) return;
|
|
6346
|
+
var platform = id.platform;
|
|
6347
|
+
if (platform !== "claude" && platform !== "openai") return;
|
|
6348
|
+
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
6349
|
+
var queueBase = id.userId || id.projectId;
|
|
6350
|
+
var queue = msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
6351
|
+
var findPlaceholder = function() {
|
|
6352
|
+
return self.state.messages.findIndex(function(m) {
|
|
6353
|
+
return m._serverItemId === serverId && m.role === "assistant" && !!m.isPending;
|
|
6354
|
+
});
|
|
6355
|
+
};
|
|
6356
|
+
var at = this.state.messages[idx] && this.state.messages[idx]._serverItemId === serverId && this.state.messages[idx].role === "assistant" ? idx : findPlaceholder();
|
|
6357
|
+
if (at !== -1) {
|
|
6358
|
+
this.state.messages[at] = Object.assign({}, this.state.messages[at], { _cancelling: true, _cancelError: void 0 });
|
|
6359
|
+
}
|
|
6360
|
+
this.host.notify();
|
|
6361
|
+
var unmark = function(errMsg) {
|
|
6362
|
+
var ci = findPlaceholder();
|
|
6363
|
+
if (ci === -1) return;
|
|
6364
|
+
self.state.messages[ci] = Object.assign({}, self.state.messages[ci], { _cancelling: false, _cancelError: errMsg });
|
|
6365
|
+
self.host.notify();
|
|
6366
|
+
};
|
|
6367
|
+
Promise.resolve(this.host.cancelRequest({
|
|
6368
|
+
url,
|
|
6369
|
+
method: "POST",
|
|
6370
|
+
id: serverId,
|
|
6371
|
+
queue,
|
|
6372
|
+
service: id.projectId,
|
|
6373
|
+
owner: id.owner
|
|
6374
|
+
})).then(function(result) {
|
|
6375
|
+
if (result && result.removed) {
|
|
6376
|
+
self._stopPoll(serverId);
|
|
6377
|
+
self._settleCancelledTurn(serverId);
|
|
6378
|
+
return;
|
|
6379
|
+
}
|
|
6380
|
+
unmark(void 0);
|
|
6381
|
+
}).catch(function(err) {
|
|
6382
|
+
unmark(err && typeof err.message === "string" && err.message ? err.message : "Could not stop this request.");
|
|
6383
|
+
});
|
|
6384
|
+
}
|
|
6385
|
+
/** The cancelled form of a request bubble, with everything that keeps it in
|
|
6386
|
+
* place carried over: the file markers that hold an indexing pass in its
|
|
6387
|
+
* collapsed row, the chat it belongs to, its send time and its render key. */
|
|
6388
|
+
_cancelledCopyOf(m, itemId) {
|
|
6389
|
+
var cancelled = { role: "user", content: m.content, isCancelled: true };
|
|
6390
|
+
if (itemId !== void 0) cancelled._serverItemId = itemId;
|
|
6391
|
+
else if (m._serverItemId !== void 0) cancelled._serverItemId = m._serverItemId;
|
|
6392
|
+
if (m.isBackgroundTask) cancelled.isBackgroundTask = true;
|
|
6393
|
+
if (m._indexFile) cancelled._indexFile = m._indexFile;
|
|
6394
|
+
if (m._useBgQueue) cancelled._useBgQueue = true;
|
|
6395
|
+
if (m._ownerKey !== void 0) cancelled._ownerKey = m._ownerKey;
|
|
6396
|
+
if (m._ts !== void 0) cancelled._ts = m._ts;
|
|
6397
|
+
if (m._localId !== void 0) cancelled._localId = m._localId;
|
|
6398
|
+
return cancelled;
|
|
6399
|
+
}
|
|
6400
|
+
/**
|
|
6401
|
+
* Settle a turn as cancelled wherever it lives, and let the queue move on.
|
|
6402
|
+
*
|
|
6403
|
+
* On the chat on screen: every pending placeholder carrying the id goes (a
|
|
6404
|
+
* history refetch can have re-mapped the running turn into a second one), the
|
|
6405
|
+
* request bubble takes its cancelled form, and the next queued turn is promoted
|
|
6406
|
+
* so the conversation does not sit on an "(In queue)" bubble with nothing
|
|
6407
|
+
* running. For a chat that is NOT on screen (the user moved on while the turn
|
|
6408
|
+
* ran, and it was cancelled from elsewhere) the same edit is made in that chat's
|
|
6409
|
+
* history cache, which is what the next visit renders.
|
|
6410
|
+
*
|
|
6411
|
+
* Idempotent on purpose: a cancel this client made settles the bubbles at once,
|
|
6412
|
+
* and the chain that was awaiting the poll settles again a moment later when the
|
|
6413
|
+
* stop resolves it. The second pass finds nothing to do. Deliberately NOT
|
|
6414
|
+
* _removeStrayPendingAssistants: by then the placeholder on screen is the freshly
|
|
6415
|
+
* promoted turn's, whose user bubble is pending, and the sweep would take it.
|
|
6416
|
+
*/
|
|
6417
|
+
_settleCancelledTurn(itemId, ownerKey) {
|
|
6418
|
+
if (!itemId) return;
|
|
6419
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
6420
|
+
this._applyCancelToCache(ownerKey, itemId);
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
var changed = false, wasBg = false;
|
|
6424
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
6425
|
+
var m = this.state.messages[i];
|
|
6426
|
+
if (!m || m._serverItemId !== itemId) continue;
|
|
6427
|
+
if (m.role === "assistant" && m.isPending) {
|
|
6428
|
+
this.state.messages.splice(i, 1);
|
|
6429
|
+
changed = true;
|
|
6430
|
+
continue;
|
|
6431
|
+
}
|
|
6432
|
+
if (m.role === "user" && !m.isCancelled) {
|
|
6433
|
+
if (m.isBackgroundTask) wasBg = true;
|
|
6434
|
+
this.state.messages[i] = this._cancelledCopyOf(m, itemId);
|
|
6435
|
+
changed = true;
|
|
6436
|
+
}
|
|
6437
|
+
}
|
|
6438
|
+
this.cancelledServerIds.delete(itemId);
|
|
6439
|
+
if (!changed) return;
|
|
6440
|
+
if (wasBg) this.promoteNextBgQueuedToRunning();
|
|
6441
|
+
else this.promoteNextQueuedToRunning();
|
|
6442
|
+
this.updateHistoryCache();
|
|
6443
|
+
this.host.notify();
|
|
6444
|
+
}
|
|
6445
|
+
/**
|
|
6446
|
+
* _settleCancelledTurn for a chat that is not on screen: the same edit, made in
|
|
6447
|
+
* its history cache. Matched by id where the cached bubbles carry one; an
|
|
6448
|
+
* off-chat send caches its pair before the ack (see dispatchComposedMessage), so
|
|
6449
|
+
* the fallback is positional, exactly as _applyReplyToCache's is: the trailing
|
|
6450
|
+
* pending placeholder, and the last request bubble still marked in flight.
|
|
6451
|
+
*/
|
|
6452
|
+
_applyCancelToCache(key, itemId) {
|
|
6453
|
+
var existing = key ? this.aiChatHistoryCache[key] : void 0;
|
|
6454
|
+
if (!existing) return;
|
|
6455
|
+
var msgs = existing.messages.slice();
|
|
6456
|
+
var changed = false;
|
|
6457
|
+
var pIdx = -1, uIdx = -1;
|
|
6458
|
+
for (var i = msgs.length - 1; i >= 0; i--) {
|
|
6459
|
+
var m = msgs[i];
|
|
6460
|
+
if (!m || m._serverItemId !== itemId) continue;
|
|
6461
|
+
if (m.role === "assistant" && m.isPending) {
|
|
6462
|
+
msgs.splice(i, 1);
|
|
6463
|
+
changed = true;
|
|
6464
|
+
pIdx = i;
|
|
6465
|
+
} else if (m.role === "user" && !m.isCancelled && uIdx === -1) uIdx = i;
|
|
6466
|
+
}
|
|
6467
|
+
if (pIdx === -1) {
|
|
6468
|
+
for (var p = msgs.length - 1; p >= 0; p--) {
|
|
6469
|
+
var pm = msgs[p];
|
|
6470
|
+
if (!pm || !pm.isPending || pm.role !== "assistant" || pm.isBackgroundTask || pm._serverItemId !== void 0) continue;
|
|
6471
|
+
msgs.splice(p, 1);
|
|
6472
|
+
changed = true;
|
|
6473
|
+
break;
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
if (uIdx === -1) {
|
|
6477
|
+
for (var u = msgs.length - 1; u >= 0; u--) {
|
|
6478
|
+
var um = msgs[u];
|
|
6479
|
+
if (!um || um.role !== "user" || um.isBackgroundTask || um._serverItemId !== void 0 || um._stageId) continue;
|
|
6480
|
+
if (!(um.isPendingQueued || um.isPendingInProcess || um.isSendingToServer)) continue;
|
|
6481
|
+
uIdx = u;
|
|
6482
|
+
break;
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
if (uIdx !== -1) {
|
|
6486
|
+
msgs[uIdx] = this._cancelledCopyOf(msgs[uIdx], itemId);
|
|
6487
|
+
changed = true;
|
|
6488
|
+
}
|
|
6489
|
+
if (!changed) return;
|
|
6490
|
+
this.aiChatHistoryCache[key] = { messages: msgs, endOfList: existing.endOfList, startKeyHistory: existing.startKeyHistory };
|
|
6491
|
+
}
|
|
6251
6492
|
/**
|
|
6252
6493
|
* Stop indexing a file, from its collapsed row — every pass at once, not just
|
|
6253
6494
|
* the bubble the user happens to see.
|
|
@@ -6314,6 +6555,7 @@ var ChatSession = class {
|
|
|
6314
6555
|
}
|
|
6315
6556
|
}
|
|
6316
6557
|
this._adoptWorkerIndexingPasses(0);
|
|
6558
|
+
this._sweepCancelledIndexing();
|
|
6317
6559
|
var ids = group.cancellableIds || [];
|
|
6318
6560
|
if (!ids.length) {
|
|
6319
6561
|
this.host.notify();
|
|
@@ -6450,7 +6692,7 @@ var ChatSession = class {
|
|
|
6450
6692
|
enqueueTypewrite(idx, fullText, localId, paintedText) {
|
|
6451
6693
|
var self = this;
|
|
6452
6694
|
var target = this.state.messages[idx];
|
|
6453
|
-
if (target && target._ts === void 0) target._ts =
|
|
6695
|
+
if (target && target._ts === void 0) target._ts = this._replyStampFor(target);
|
|
6454
6696
|
if (!this.typewriterQueue) this.typewriterQueue = Promise.resolve();
|
|
6455
6697
|
this.typewriterQueue = this.typewriterQueue.then(function() {
|
|
6456
6698
|
return self.typewriteIntoIndex(idx, fullText, localId, paintedText);
|
|
@@ -6596,9 +6838,10 @@ var ChatSession = class {
|
|
|
6596
6838
|
this.state.sending = true;
|
|
6597
6839
|
this.host.scrollToBottomIfSticky(true);
|
|
6598
6840
|
return Promise.resolve(pending).catch(function() {
|
|
6599
|
-
}).then(function() {
|
|
6841
|
+
}).then(function(result) {
|
|
6600
6842
|
if (token !== self.state.gateRefreshToken) return;
|
|
6601
6843
|
self.state.sending = false;
|
|
6844
|
+
if (result && result.cancelled) return;
|
|
6602
6845
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
6603
6846
|
self.host.scrollToBottomIfSticky(true);
|
|
6604
6847
|
});
|
|
@@ -8227,8 +8470,19 @@ function buildChatDisplayList(messages, opts) {
|
|
|
8227
8470
|
} else if (stopped) {
|
|
8228
8471
|
grp.status = "cancelled";
|
|
8229
8472
|
} else {
|
|
8230
|
-
var
|
|
8231
|
-
|
|
8473
|
+
var retriedIds = {};
|
|
8474
|
+
for (var ri = 0; ri < grp.members.length; ri++) {
|
|
8475
|
+
var rm = grp.members[ri].msg;
|
|
8476
|
+
if (rm.isRetried && rm._serverItemId) retriedIds[rm._serverItemId] = true;
|
|
8477
|
+
}
|
|
8478
|
+
var li = grp.members.length - 1;
|
|
8479
|
+
while (li >= 0 && (grp.members[li].msg.isRetried || grp.members[li].msg._serverItemId && retriedIds[grp.members[li].msg._serverItemId])) li--;
|
|
8480
|
+
if (li < 0) {
|
|
8481
|
+
grp.status = "active";
|
|
8482
|
+
} else {
|
|
8483
|
+
var last = grp.members[li].msg;
|
|
8484
|
+
grp.status = last.isError ? "error" : "done";
|
|
8485
|
+
}
|
|
8232
8486
|
}
|
|
8233
8487
|
var sawFirstPass = false;
|
|
8234
8488
|
for (var pi = 0; pi < grp.members.length; pi++) {
|
|
@@ -8693,6 +8947,7 @@ exports.isOfficeFile = isOfficeFile;
|
|
|
8693
8947
|
exports.isPagedReadFile = isPagedReadFile;
|
|
8694
8948
|
exports.isPreviewableImagePath = isPreviewableImagePath;
|
|
8695
8949
|
exports.isProviderApiKeyError = isProviderApiKeyError;
|
|
8950
|
+
exports.isRetriedFailure = isRetriedFailure;
|
|
8696
8951
|
exports.isServerExtractable = isServerExtractable;
|
|
8697
8952
|
exports.isServiceDbAttachmentHref = isServiceDbAttachmentHref;
|
|
8698
8953
|
exports.isWindowedReadFile = isWindowedReadFile;
|