bunnyquery 1.11.0 → 1.12.1
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 +260 -6
- package/dist/engine.cjs +243 -5
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +79 -1
- package/dist/engine.d.ts +79 -1
- package/dist/engine.mjs +243 -6
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +13 -3
- package/src/engine/errors.ts +15 -0
- package/src/engine/history.ts +5 -1
- 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/session.ts +239 -3
- package/styles/chat.css +59 -0
package/README.md
CHANGED
|
@@ -34,6 +34,11 @@ you can build your own chat UI on top of it. See
|
|
|
34
34
|
across as many passes as it takes. A file's passes collapse into a single
|
|
35
35
|
status row in the chat that can be expanded, and stopped: "Stop" cancels every
|
|
36
36
|
queued and running pass at once and ends the continuation chain.
|
|
37
|
+
- **Stop waiting**: a reply that is taking too long can be given up on. The
|
|
38
|
+
"Thinking" bubble carries a small x that cancels the running request on the
|
|
39
|
+
server, marks the question as cancelled, and lets the next message go out
|
|
40
|
+
at once instead of queueing behind it. A message still waiting in the queue
|
|
41
|
+
has the same control on its own bubble.
|
|
37
42
|
- **Attachment parser plugins**: register a client-side parser so the widget
|
|
38
43
|
extracts text in the browser from formats the model can't otherwise read, and
|
|
39
44
|
indexes it directly. See [Attachment parser plugins](#attachment-parser-plugins).
|
package/bunnyquery.css
CHANGED
|
@@ -1175,6 +1175,28 @@
|
|
|
1175
1175
|
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble:has(> .bq-loader) > .bq-loader {
|
|
1176
1176
|
vertical-align: middle;
|
|
1177
1177
|
}
|
|
1178
|
+
/* The dot-trail cycles '.', '..', '...', so left to itself it is a box that
|
|
1179
|
+
changes width three times every 1.2s: the bubble pulsed with it, and the
|
|
1180
|
+
stop-waiting control that follows the dots moved each time, which made it hard
|
|
1181
|
+
to hit. Inside a bubble the loader therefore RESERVES the width of its widest
|
|
1182
|
+
frame: a hidden '...' sets the box and the animated dots are laid over it from
|
|
1183
|
+
the left. Sized by the text itself rather than by a length, so it is right in
|
|
1184
|
+
any font and at any letter-spacing. Scoped to bubbles on purpose: a loader that
|
|
1185
|
+
trails a label ("Saving...") has to keep hugging its text. */
|
|
1186
|
+
.bq-bubble > .bq-loader {
|
|
1187
|
+
position: relative;
|
|
1188
|
+
display: inline-block;
|
|
1189
|
+
white-space: nowrap;
|
|
1190
|
+
}
|
|
1191
|
+
.bq-bubble > .bq-loader::before {
|
|
1192
|
+
content: '...';
|
|
1193
|
+
visibility: hidden;
|
|
1194
|
+
}
|
|
1195
|
+
.bq-bubble > .bq-loader::after {
|
|
1196
|
+
position: absolute;
|
|
1197
|
+
left: 0;
|
|
1198
|
+
top: 0;
|
|
1199
|
+
}
|
|
1178
1200
|
|
|
1179
1201
|
.bq-message.is-error .bq-bubble {
|
|
1180
1202
|
background: var(--bq-danger-bg);
|
|
@@ -1187,6 +1209,14 @@
|
|
|
1187
1209
|
color: var(--bq-danger);
|
|
1188
1210
|
opacity: 0.65;
|
|
1189
1211
|
}
|
|
1212
|
+
/* A failed pass the worker has already sent again: real, but not the file's
|
|
1213
|
+
outcome (the replacement pass is), so it is told in the muted voice of a
|
|
1214
|
+
note rather than the red of a dead end. */
|
|
1215
|
+
.bq-message.is-error.is-retried .bq-bubble {
|
|
1216
|
+
background: var(--bq-paper);
|
|
1217
|
+
border-color: var(--bq-line);
|
|
1218
|
+
color: var(--bq-muted);
|
|
1219
|
+
}
|
|
1190
1220
|
.bq-message.is-pending-older .bq-bubble {
|
|
1191
1221
|
background: var(--bq-warning-bg);
|
|
1192
1222
|
border-color: var(--bq-warning-border);
|
|
@@ -1245,6 +1275,35 @@
|
|
|
1245
1275
|
}
|
|
1246
1276
|
.bq-cancel-queue-btn:hover:not(.is-disabled) { background: var(--bq-warning-bg); color: var(--bq-warning); }
|
|
1247
1277
|
.bq-cancel-queue-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
|
|
1278
|
+
/* Stop waiting for a reply: sits INLINE after the dot-trail in a chat turn's own
|
|
1279
|
+
"Thinking" bubble, where the bunny is inline too (:has(> .bq-loader) above).
|
|
1280
|
+
Same face as the queued-message × but placed differently: that one floats to
|
|
1281
|
+
its bubble's corner, this one simply follows the dots. Muted at rest so the
|
|
1282
|
+
dots stay the thing the eye lands on; hover changes opacity only, never its
|
|
1283
|
+
colour. Shared by both chatboxes. */
|
|
1284
|
+
.bq-cancel-wait-btn {
|
|
1285
|
+
display: inline-flex;
|
|
1286
|
+
align-items: center;
|
|
1287
|
+
justify-content: center;
|
|
1288
|
+
vertical-align: middle;
|
|
1289
|
+
margin-left: 0.55rem;
|
|
1290
|
+
width: 1.1rem;
|
|
1291
|
+
height: 1.1rem;
|
|
1292
|
+
min-height: 0;
|
|
1293
|
+
padding: 0;
|
|
1294
|
+
border: 1px solid var(--bq-line);
|
|
1295
|
+
background: transparent;
|
|
1296
|
+
color: var(--bq-muted);
|
|
1297
|
+
font-size: 0.85rem;
|
|
1298
|
+
line-height: 1;
|
|
1299
|
+
cursor: pointer;
|
|
1300
|
+
box-shadow: none;
|
|
1301
|
+
border-radius: 0;
|
|
1302
|
+
flex-shrink: 0;
|
|
1303
|
+
opacity: 0.7;
|
|
1304
|
+
}
|
|
1305
|
+
.bq-cancel-wait-btn:hover:not(.is-disabled) { opacity: 1; }
|
|
1306
|
+
.bq-cancel-wait-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
|
|
1248
1307
|
|
|
1249
1308
|
/* ---- unfinalized streamed answer (ask for it) ----------------------------
|
|
1250
1309
|
A turn whose answer was streamed and never finalized keeps its bytes in the
|
package/bunnyquery.js
CHANGED
|
@@ -619,6 +619,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
619
619
|
}
|
|
620
620
|
return "Something went wrong.";
|
|
621
621
|
}
|
|
622
|
+
function isRetriedFailure(input) {
|
|
623
|
+
var envErr = csrEnvelopeError(input);
|
|
624
|
+
if (envErr !== void 0) input = envErr;
|
|
625
|
+
return !!input && typeof input === "object" && input.retried === true;
|
|
626
|
+
}
|
|
622
627
|
function isErrorResponseBody(response) {
|
|
623
628
|
var envErr = csrEnvelopeError(response);
|
|
624
629
|
if (envErr !== void 0) response = envErr;
|
|
@@ -1024,6 +1029,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1024
1029
|
var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
|
|
1025
1030
|
var CONTEXT_WINDOW_BY_MODEL = {
|
|
1026
1031
|
// claude, exact ids
|
|
1032
|
+
"claude-fable-5-1": 1e6,
|
|
1033
|
+
"claude-opus-5-5": 1e6,
|
|
1034
|
+
"claude-sonnet-5-5": 1e6,
|
|
1027
1035
|
"claude-fable-5": 1e6,
|
|
1028
1036
|
"claude-opus-5": 1e6,
|
|
1029
1037
|
"claude-opus-4-8": 1e6,
|
|
@@ -1037,6 +1045,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1037
1045
|
"claude-haiku-4-5": 2e5,
|
|
1038
1046
|
"claude-3-5-sonnet": 2e5,
|
|
1039
1047
|
// openai, exact ids
|
|
1048
|
+
"gpt-6-astra": 105e4,
|
|
1049
|
+
"gpt-6-sol": 105e4,
|
|
1050
|
+
"gpt-6-luna": 105e4,
|
|
1040
1051
|
"gpt-5.6-sol": 105e4,
|
|
1041
1052
|
"gpt-5.6-terra": 105e4,
|
|
1042
1053
|
"gpt-5.6-luna": 105e4,
|
|
@@ -1053,11 +1064,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1053
1064
|
"claude-opus": 1e6,
|
|
1054
1065
|
"claude-sonnet": 1e6,
|
|
1055
1066
|
"claude-haiku": 2e5,
|
|
1067
|
+
"gpt-6": 105e4,
|
|
1056
1068
|
"gpt-5.6": 105e4,
|
|
1057
1069
|
"gpt-5": 128e3
|
|
1058
1070
|
};
|
|
1059
1071
|
var MAX_OUTPUT_BY_MODEL = {
|
|
1060
1072
|
// claude
|
|
1073
|
+
"claude-fable-5-1": 128e3,
|
|
1074
|
+
"claude-opus-5-5": 128e3,
|
|
1075
|
+
"claude-sonnet-5-5": 128e3,
|
|
1061
1076
|
"claude-fable-5": 128e3,
|
|
1062
1077
|
"claude-opus-5": 128e3,
|
|
1063
1078
|
"claude-opus-4-8": 128e3,
|
|
@@ -1066,6 +1081,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1066
1081
|
"claude-haiku-4-5": 64e3,
|
|
1067
1082
|
"claude-3-5-sonnet": 8e3,
|
|
1068
1083
|
// openai
|
|
1084
|
+
"gpt-6-astra": 128e3,
|
|
1085
|
+
"gpt-6-sol": 128e3,
|
|
1086
|
+
"gpt-6-luna": 128e3,
|
|
1069
1087
|
"gpt-5.6-sol": 128e3,
|
|
1070
1088
|
"gpt-5.6-terra": 128e3,
|
|
1071
1089
|
"gpt-5.6-luna": 128e3,
|
|
@@ -1082,6 +1100,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1082
1100
|
"claude-opus": 128e3,
|
|
1083
1101
|
"claude-sonnet": 64e3,
|
|
1084
1102
|
"claude-haiku": 64e3,
|
|
1103
|
+
"gpt-6": 128e3,
|
|
1085
1104
|
"gpt-5.6": 128e3,
|
|
1086
1105
|
"gpt-5": 128e3
|
|
1087
1106
|
};
|
|
@@ -3140,6 +3159,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3140
3159
|
mapped.push(ph);
|
|
3141
3160
|
} else if (isQueued) ; else if (isErrorResponse) {
|
|
3142
3161
|
var em = { role: "assistant", content: getErrorMessage(response), isError: true };
|
|
3162
|
+
if (isRetriedFailure(response)) em.isRetried = true;
|
|
3143
3163
|
if (item._fromBgChain) em._fromBgChain = true;
|
|
3144
3164
|
if (item._isBgTask) em.isBackgroundTask = true;
|
|
3145
3165
|
if (serverItemId !== void 0) em._serverItemId = serverItemId;
|
|
@@ -4168,7 +4188,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4168
4188
|
timers = [];
|
|
4169
4189
|
};
|
|
4170
4190
|
var stopBase = base && typeof base.stop === "function" ? base.stop.bind(base) : null;
|
|
4191
|
+
var resolveRaced = null;
|
|
4171
4192
|
var raced = new Promise(function(resolve, reject) {
|
|
4193
|
+
resolveRaced = resolve;
|
|
4172
4194
|
base.then(function(res) {
|
|
4173
4195
|
if (settled) return;
|
|
4174
4196
|
settled = true;
|
|
@@ -4209,9 +4231,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4209
4231
|
});
|
|
4210
4232
|
});
|
|
4211
4233
|
raced.stop = function() {
|
|
4234
|
+
if (settled) return;
|
|
4212
4235
|
settled = true;
|
|
4213
4236
|
clearProbes();
|
|
4214
|
-
if (stopBase)
|
|
4237
|
+
if (stopBase) {
|
|
4238
|
+
try {
|
|
4239
|
+
stopBase();
|
|
4240
|
+
} catch (e) {
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
if (resolveRaced) resolveRaced({ id: itemId, status: "stopped" });
|
|
4215
4244
|
};
|
|
4216
4245
|
return raced;
|
|
4217
4246
|
}
|
|
@@ -5224,6 +5253,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5224
5253
|
}
|
|
5225
5254
|
return response;
|
|
5226
5255
|
}).then(function(response) {
|
|
5256
|
+
if (isPollStopped(response) || self._isCancelledPollResult(response)) return { content: "", isError: false, cancelled: true };
|
|
5227
5257
|
if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
|
|
5228
5258
|
var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
|
|
5229
5259
|
answer = (answer || "").trim();
|
|
@@ -5233,6 +5263,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5233
5263
|
}).then(function(result) {
|
|
5234
5264
|
delete self.pendingAgentRequests[params.key];
|
|
5235
5265
|
if (dispatchItemId) self.historyItemPolls.delete(dispatchItemId);
|
|
5266
|
+
if (result.cancelled) {
|
|
5267
|
+
if (dispatchItemId) self._settleCancelledTurn(dispatchItemId, params.key);
|
|
5268
|
+
return result;
|
|
5269
|
+
}
|
|
5236
5270
|
var reply = { role: "assistant", content: result.content, isError: result.isError };
|
|
5237
5271
|
if (dispatchItemId) reply._serverItemId = dispatchItemId;
|
|
5238
5272
|
self._applyReplyToCache(params.key, reply, dispatchItemId);
|
|
@@ -5711,8 +5745,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5711
5745
|
}
|
|
5712
5746
|
});
|
|
5713
5747
|
Promise.resolve(run).catch(function() {
|
|
5714
|
-
}).then(function() {
|
|
5748
|
+
}).then(function(result) {
|
|
5715
5749
|
self.state.sending = false;
|
|
5750
|
+
if (result && result.cancelled) return;
|
|
5716
5751
|
if (!(self.host.isViewMounted() && self.getHistoryCacheKey() === key)) return;
|
|
5717
5752
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
5718
5753
|
self.host.scrollToBottomIfSticky(true);
|
|
@@ -5891,6 +5926,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5891
5926
|
}
|
|
5892
5927
|
onQueuedSendResponse(_composed, response, platform, serverId, ownerKey) {
|
|
5893
5928
|
if (serverId) this.historyItemPolls.delete(serverId);
|
|
5929
|
+
if (this._isCancelledPollResult(response)) {
|
|
5930
|
+
if (serverId) this._settleCancelledTurn(serverId, ownerKey);
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5894
5933
|
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
5895
5934
|
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." };
|
|
5896
5935
|
this._applyReplyToCache(ownerKey, offReply, serverId);
|
|
@@ -6090,6 +6129,191 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6090
6129
|
}
|
|
6091
6130
|
});
|
|
6092
6131
|
}
|
|
6132
|
+
/**
|
|
6133
|
+
* Stop waiting for the reply to the chat turn that is running now, and move on.
|
|
6134
|
+
*
|
|
6135
|
+
* `msg` is the turn's pending ASSISTANT bubble (the dot-trail "Thinking"
|
|
6136
|
+
* placeholder), which is where both views draw the control. A turn's two bubbles
|
|
6137
|
+
* carry the same _serverItemId, so it is also how the request is found.
|
|
6138
|
+
* cancelQueuedMessage is the sibling for a turn still waiting in the queue and
|
|
6139
|
+
* takes the USER bubble; the two differ in what they are handed and in what has
|
|
6140
|
+
* to be unwound afterwards, not in what the server is asked.
|
|
6141
|
+
*
|
|
6142
|
+
* What ends the wait:
|
|
6143
|
+
* 1. csr-cancel marks the running row cancelled. The worker stops relaying and
|
|
6144
|
+
* never stores an answer for it, so the reply cannot turn up later on this
|
|
6145
|
+
* device or another, and history replays the turn as cancelled;
|
|
6146
|
+
* 2. the poll on it is stopped, which settles whichever chain was awaiting it
|
|
6147
|
+
* with a stopped result: dispatchAgentRequest's for an immediate send, the
|
|
6148
|
+
* queued chain for a promoted turn, the history poll after a reload. Each
|
|
6149
|
+
* reads a stopped or cancelled result as "no reply" and releases what it
|
|
6150
|
+
* holds (state.sending, pendingAgentRequests, historyItemPolls), which is
|
|
6151
|
+
* what lets the next send go out immediately instead of queueing;
|
|
6152
|
+
* 3. the bubbles settle at once, without waiting for that chain: the user
|
|
6153
|
+
* bubble takes its cancelled form, the placeholder goes, and the next queued
|
|
6154
|
+
* turn, if there is one, is promoted so its own "Thinking" appears.
|
|
6155
|
+
*
|
|
6156
|
+
* What it does NOT do: the provider call already in flight on the worker runs
|
|
6157
|
+
* to its end (a streamed one stops within about a second). A message sent
|
|
6158
|
+
* behind it on the same queue starts once the worker acks the cancelled row.
|
|
6159
|
+
*
|
|
6160
|
+
* Refused without a server id (the ack has not come back yet; the views disable
|
|
6161
|
+
* the control until it has) and while a cancel is already out. Indexing passes
|
|
6162
|
+
* are not this method's business: their Stop is cancelIndexingGroup, which ends
|
|
6163
|
+
* the file's whole chain.
|
|
6164
|
+
*/
|
|
6165
|
+
cancelPendingReply(msg, idx) {
|
|
6166
|
+
var self = this;
|
|
6167
|
+
var id = this.host.getIdentity();
|
|
6168
|
+
var serverId = msg._serverItemId;
|
|
6169
|
+
if (!serverId || msg._cancelling) return;
|
|
6170
|
+
if (msg.role !== "assistant" || !msg.isPending || msg.isBackgroundTask) return;
|
|
6171
|
+
var platform = id.platform;
|
|
6172
|
+
if (platform !== "claude" && platform !== "openai") return;
|
|
6173
|
+
var url = platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
6174
|
+
var queueBase = id.userId || id.projectId;
|
|
6175
|
+
var queue = msg._useBgQueue ? bgIndexingQueueName(queueBase) : queueBase;
|
|
6176
|
+
var findPlaceholder = function() {
|
|
6177
|
+
return self.state.messages.findIndex(function(m) {
|
|
6178
|
+
return m._serverItemId === serverId && m.role === "assistant" && !!m.isPending;
|
|
6179
|
+
});
|
|
6180
|
+
};
|
|
6181
|
+
var at = this.state.messages[idx] && this.state.messages[idx]._serverItemId === serverId && this.state.messages[idx].role === "assistant" ? idx : findPlaceholder();
|
|
6182
|
+
if (at !== -1) {
|
|
6183
|
+
this.state.messages[at] = Object.assign({}, this.state.messages[at], { _cancelling: true, _cancelError: void 0 });
|
|
6184
|
+
}
|
|
6185
|
+
this.host.notify();
|
|
6186
|
+
var unmark = function(errMsg) {
|
|
6187
|
+
var ci = findPlaceholder();
|
|
6188
|
+
if (ci === -1) return;
|
|
6189
|
+
self.state.messages[ci] = Object.assign({}, self.state.messages[ci], { _cancelling: false, _cancelError: errMsg });
|
|
6190
|
+
self.host.notify();
|
|
6191
|
+
};
|
|
6192
|
+
Promise.resolve(this.host.cancelRequest({
|
|
6193
|
+
url,
|
|
6194
|
+
method: "POST",
|
|
6195
|
+
id: serverId,
|
|
6196
|
+
queue,
|
|
6197
|
+
service: id.projectId,
|
|
6198
|
+
owner: id.owner
|
|
6199
|
+
})).then(function(result) {
|
|
6200
|
+
if (result && result.removed) {
|
|
6201
|
+
self._stopPoll(serverId);
|
|
6202
|
+
self._settleCancelledTurn(serverId);
|
|
6203
|
+
return;
|
|
6204
|
+
}
|
|
6205
|
+
unmark(void 0);
|
|
6206
|
+
}).catch(function(err) {
|
|
6207
|
+
unmark(err && typeof err.message === "string" && err.message ? err.message : "Could not stop this request.");
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
/** The cancelled form of a request bubble, with everything that keeps it in
|
|
6211
|
+
* place carried over: the file markers that hold an indexing pass in its
|
|
6212
|
+
* collapsed row, the chat it belongs to, its send time and its render key. */
|
|
6213
|
+
_cancelledCopyOf(m, itemId) {
|
|
6214
|
+
var cancelled = { role: "user", content: m.content, isCancelled: true };
|
|
6215
|
+
if (itemId !== void 0) cancelled._serverItemId = itemId;
|
|
6216
|
+
else if (m._serverItemId !== void 0) cancelled._serverItemId = m._serverItemId;
|
|
6217
|
+
if (m.isBackgroundTask) cancelled.isBackgroundTask = true;
|
|
6218
|
+
if (m._indexFile) cancelled._indexFile = m._indexFile;
|
|
6219
|
+
if (m._useBgQueue) cancelled._useBgQueue = true;
|
|
6220
|
+
if (m._ownerKey !== void 0) cancelled._ownerKey = m._ownerKey;
|
|
6221
|
+
if (m._ts !== void 0) cancelled._ts = m._ts;
|
|
6222
|
+
if (m._localId !== void 0) cancelled._localId = m._localId;
|
|
6223
|
+
return cancelled;
|
|
6224
|
+
}
|
|
6225
|
+
/**
|
|
6226
|
+
* Settle a turn as cancelled wherever it lives, and let the queue move on.
|
|
6227
|
+
*
|
|
6228
|
+
* On the chat on screen: every pending placeholder carrying the id goes (a
|
|
6229
|
+
* history refetch can have re-mapped the running turn into a second one), the
|
|
6230
|
+
* request bubble takes its cancelled form, and the next queued turn is promoted
|
|
6231
|
+
* so the conversation does not sit on an "(In queue)" bubble with nothing
|
|
6232
|
+
* running. For a chat that is NOT on screen (the user moved on while the turn
|
|
6233
|
+
* ran, and it was cancelled from elsewhere) the same edit is made in that chat's
|
|
6234
|
+
* history cache, which is what the next visit renders.
|
|
6235
|
+
*
|
|
6236
|
+
* Idempotent on purpose: a cancel this client made settles the bubbles at once,
|
|
6237
|
+
* and the chain that was awaiting the poll settles again a moment later when the
|
|
6238
|
+
* stop resolves it. The second pass finds nothing to do. Deliberately NOT
|
|
6239
|
+
* _removeStrayPendingAssistants: by then the placeholder on screen is the freshly
|
|
6240
|
+
* promoted turn's, whose user bubble is pending, and the sweep would take it.
|
|
6241
|
+
*/
|
|
6242
|
+
_settleCancelledTurn(itemId, ownerKey) {
|
|
6243
|
+
if (!itemId) return;
|
|
6244
|
+
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
6245
|
+
this._applyCancelToCache(ownerKey, itemId);
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
var changed = false, wasBg = false;
|
|
6249
|
+
for (var i = this.state.messages.length - 1; i >= 0; i--) {
|
|
6250
|
+
var m = this.state.messages[i];
|
|
6251
|
+
if (!m || m._serverItemId !== itemId) continue;
|
|
6252
|
+
if (m.role === "assistant" && m.isPending) {
|
|
6253
|
+
this.state.messages.splice(i, 1);
|
|
6254
|
+
changed = true;
|
|
6255
|
+
continue;
|
|
6256
|
+
}
|
|
6257
|
+
if (m.role === "user" && !m.isCancelled) {
|
|
6258
|
+
if (m.isBackgroundTask) wasBg = true;
|
|
6259
|
+
this.state.messages[i] = this._cancelledCopyOf(m, itemId);
|
|
6260
|
+
changed = true;
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
this.cancelledServerIds.delete(itemId);
|
|
6264
|
+
if (!changed) return;
|
|
6265
|
+
if (wasBg) this.promoteNextBgQueuedToRunning();
|
|
6266
|
+
else this.promoteNextQueuedToRunning();
|
|
6267
|
+
this.updateHistoryCache();
|
|
6268
|
+
this.host.notify();
|
|
6269
|
+
}
|
|
6270
|
+
/**
|
|
6271
|
+
* _settleCancelledTurn for a chat that is not on screen: the same edit, made in
|
|
6272
|
+
* its history cache. Matched by id where the cached bubbles carry one; an
|
|
6273
|
+
* off-chat send caches its pair before the ack (see dispatchComposedMessage), so
|
|
6274
|
+
* the fallback is positional, exactly as _applyReplyToCache's is: the trailing
|
|
6275
|
+
* pending placeholder, and the last request bubble still marked in flight.
|
|
6276
|
+
*/
|
|
6277
|
+
_applyCancelToCache(key, itemId) {
|
|
6278
|
+
var existing = key ? this.aiChatHistoryCache[key] : void 0;
|
|
6279
|
+
if (!existing) return;
|
|
6280
|
+
var msgs = existing.messages.slice();
|
|
6281
|
+
var changed = false;
|
|
6282
|
+
var pIdx = -1, uIdx = -1;
|
|
6283
|
+
for (var i = msgs.length - 1; i >= 0; i--) {
|
|
6284
|
+
var m = msgs[i];
|
|
6285
|
+
if (!m || m._serverItemId !== itemId) continue;
|
|
6286
|
+
if (m.role === "assistant" && m.isPending) {
|
|
6287
|
+
msgs.splice(i, 1);
|
|
6288
|
+
changed = true;
|
|
6289
|
+
pIdx = i;
|
|
6290
|
+
} else if (m.role === "user" && !m.isCancelled && uIdx === -1) uIdx = i;
|
|
6291
|
+
}
|
|
6292
|
+
if (pIdx === -1) {
|
|
6293
|
+
for (var p = msgs.length - 1; p >= 0; p--) {
|
|
6294
|
+
var pm = msgs[p];
|
|
6295
|
+
if (!pm || !pm.isPending || pm.role !== "assistant" || pm.isBackgroundTask || pm._serverItemId !== void 0) continue;
|
|
6296
|
+
msgs.splice(p, 1);
|
|
6297
|
+
changed = true;
|
|
6298
|
+
break;
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
if (uIdx === -1) {
|
|
6302
|
+
for (var u = msgs.length - 1; u >= 0; u--) {
|
|
6303
|
+
var um = msgs[u];
|
|
6304
|
+
if (!um || um.role !== "user" || um.isBackgroundTask || um._serverItemId !== void 0 || um._stageId) continue;
|
|
6305
|
+
if (!(um.isPendingQueued || um.isPendingInProcess || um.isSendingToServer)) continue;
|
|
6306
|
+
uIdx = u;
|
|
6307
|
+
break;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
if (uIdx !== -1) {
|
|
6311
|
+
msgs[uIdx] = this._cancelledCopyOf(msgs[uIdx], itemId);
|
|
6312
|
+
changed = true;
|
|
6313
|
+
}
|
|
6314
|
+
if (!changed) return;
|
|
6315
|
+
this.aiChatHistoryCache[key] = { messages: msgs, endOfList: existing.endOfList, startKeyHistory: existing.startKeyHistory };
|
|
6316
|
+
}
|
|
6093
6317
|
/**
|
|
6094
6318
|
* Stop indexing a file, from its collapsed row — every pass at once, not just
|
|
6095
6319
|
* the bubble the user happens to see.
|
|
@@ -6156,6 +6380,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6156
6380
|
}
|
|
6157
6381
|
}
|
|
6158
6382
|
this._adoptWorkerIndexingPasses(0);
|
|
6383
|
+
this._sweepCancelledIndexing();
|
|
6159
6384
|
var ids = group.cancellableIds || [];
|
|
6160
6385
|
if (!ids.length) {
|
|
6161
6386
|
this.host.notify();
|
|
@@ -6438,9 +6663,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6438
6663
|
this.state.sending = true;
|
|
6439
6664
|
this.host.scrollToBottomIfSticky(true);
|
|
6440
6665
|
return Promise.resolve(pending).catch(function() {
|
|
6441
|
-
}).then(function() {
|
|
6666
|
+
}).then(function(result) {
|
|
6442
6667
|
if (token !== self.state.gateRefreshToken) return;
|
|
6443
6668
|
self.state.sending = false;
|
|
6669
|
+
if (result && result.cancelled) return;
|
|
6444
6670
|
return Promise.resolve(self.typewriteLatestReply(key)).then(function() {
|
|
6445
6671
|
self.host.scrollToBottomIfSticky(true);
|
|
6446
6672
|
});
|
|
@@ -8069,8 +8295,19 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8069
8295
|
} else if (stopped) {
|
|
8070
8296
|
grp.status = "cancelled";
|
|
8071
8297
|
} else {
|
|
8072
|
-
var
|
|
8073
|
-
|
|
8298
|
+
var retriedIds = {};
|
|
8299
|
+
for (var ri = 0; ri < grp.members.length; ri++) {
|
|
8300
|
+
var rm = grp.members[ri].msg;
|
|
8301
|
+
if (rm.isRetried && rm._serverItemId) retriedIds[rm._serverItemId] = true;
|
|
8302
|
+
}
|
|
8303
|
+
var li = grp.members.length - 1;
|
|
8304
|
+
while (li >= 0 && (grp.members[li].msg.isRetried || grp.members[li].msg._serverItemId && retriedIds[grp.members[li].msg._serverItemId])) li--;
|
|
8305
|
+
if (li < 0) {
|
|
8306
|
+
grp.status = "active";
|
|
8307
|
+
} else {
|
|
8308
|
+
var last = grp.members[li].msg;
|
|
8309
|
+
grp.status = last.isError ? "error" : "done";
|
|
8310
|
+
}
|
|
8074
8311
|
}
|
|
8075
8312
|
var sawFirstPass = false;
|
|
8076
8313
|
for (var pi = 0; pi < grp.members.length; pi++) {
|
|
@@ -8360,7 +8597,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8360
8597
|
(function() {
|
|
8361
8598
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
8362
8599
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
8363
|
-
var BQ_VERSION = "1.
|
|
8600
|
+
var BQ_VERSION = "1.12.1" ;
|
|
8364
8601
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
8365
8602
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8366
8603
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -11643,12 +11880,29 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11643
11880
|
var cls = ["bq-message"];
|
|
11644
11881
|
cls.push(msg.role === "user" ? "is-user" : "is-assistant");
|
|
11645
11882
|
if (msg.isError) cls.push("is-error");
|
|
11883
|
+
if (msg.isRetried) cls.push("is-retried");
|
|
11646
11884
|
if (msg.isCancelled) cls.push("is-cancelled");
|
|
11647
11885
|
if (msg.isPendingQueued || msg.isPendingOlder) cls.push("is-pending-older");
|
|
11648
11886
|
if (msg._dimSending || msg._cancelling) cls.push("is-sending-to-server");
|
|
11649
11887
|
var bubble;
|
|
11650
11888
|
if (msg.isPending && !msg._streaming || streamRecoveryPhase(msg) === "active") {
|
|
11651
11889
|
bubble = h("div", { class: "bq-bubble" }, h("span", { class: "bq-loader" }));
|
|
11890
|
+
if (msg.role === "assistant" && !msg.isBackgroundTask && !streamRecoveryPhase(msg)) {
|
|
11891
|
+
var waitDisabled = !msg._serverItemId || msg._cancelling;
|
|
11892
|
+
var stopWaitBtn = h("button", {
|
|
11893
|
+
class: "bq-cancel-wait-btn" + (waitDisabled ? " is-disabled" : ""),
|
|
11894
|
+
type: "button",
|
|
11895
|
+
title: "Stop waiting for this reply",
|
|
11896
|
+
"aria-label": "Stop waiting for this reply",
|
|
11897
|
+
html: "×"
|
|
11898
|
+
});
|
|
11899
|
+
if (!waitDisabled) stopWaitBtn.addEventListener("click", function(e) {
|
|
11900
|
+
e.stopPropagation();
|
|
11901
|
+
session.cancelPendingReply(msg, idx);
|
|
11902
|
+
});
|
|
11903
|
+
bubble.appendChild(stopWaitBtn);
|
|
11904
|
+
if (msg._cancelError) bubble.appendChild(h("span", { class: "bq-cancel-error", text: msg._cancelError }));
|
|
11905
|
+
}
|
|
11652
11906
|
} else if (streamRecoveryPhase(msg)) {
|
|
11653
11907
|
var labels = streamRecoveryLabels(streamRecoveryPhase(msg));
|
|
11654
11908
|
bubble = h("div", { class: "bq-bubble is-stream-recover" + (streamRecoveryPhase(msg) === "failed" ? " is-stream-failed" : "") });
|