lavish-axi 0.1.67 → 0.1.68
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 +2 -2
- package/dist/chrome-client.js +407 -100
- package/dist/cli.mjs +5 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -198,8 +198,8 @@ pnpm link
|
|
|
198
198
|
For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
|
|
199
199
|
Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
|
|
200
200
|
On wider screens, queued annotation preview pills and chat history share a scrollable Conversation panel above a sticky composer, so long feedback queues do not push the text box or send controls off screen.
|
|
201
|
-
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session), while the composer exposes **Send & End** beside **Send to Agent** to submit queued prompts and user-ended attribution together.
|
|
202
|
-
Composer feedback is visibly queued before the text box clears and stays queued until acknowledged,
|
|
201
|
+
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session), while the composer exposes **Send & End** beside **Send to Agent** to submit queued prompts and user-ended attribution together. Once **Send & End** starts, Lavish pauses new feedback, waits up to 5 seconds for feedback preparation already underway, keeps any open annotation draft intact until delivery succeeds, then holds that exact terminal batch. A completed reservation survives a page reload, including when its prompts were already delivered and only the pending end remains; incomplete preparation is not restored. If preparation times out, Lavish keeps the review open and its existing queue editable with a visible error. A transient delivery failure permits only retrying the same terminal batch through **Send & End** until it succeeds. If a snapshot makes the request too large (HTTP 413), Lavish retries that exact batch once without the optional snapshot. An actionable attachment rejection (HTTP 400), recoverable layout-selection conflict (HTTP 409), or unrecoverable 413 instead cancels the terminal reservation without ending the session, keeps the prompts queued, and restores editing, removal, **Send to Agent**, **Send & End**, and **End session** so the reviewer can revise or remove stale feedback before submitting again.
|
|
202
|
+
Composer feedback is visibly queued before the text box clears and stays queued until acknowledged. Send waits up to 5 seconds for the artifact's DOM snapshot, then delivers without that optional context if the frame no longer answers; a late snapshot cannot submit the batch twice. A failed or still-unacknowledged POST keeps the queue and shows persistent recovery guidance.
|
|
203
203
|
- **Reviewing on a phone** - Below 860px wide, the artifact takes the whole screen above a **Conversation** dock, and the conversation opens as a bottom sheet over it: tap the dock, swipe it up, or press the chevron to raise it; tap the dimmed artifact, swipe the sheet down, press the chevron, or press Escape to lower it. The dock reports what matters while the sheet is down - how many prompts are queued, a reply that arrived while you were reading, or whether the agent is listening - and the sheet stays open across a reload of the review page. The sheet sizes itself to the visible viewport and respects safe-area insets; if the keyboard or attachments leave little room, conversation content yields or scrolls while the send actions remain pinned above the bottom edge. In landscape the sheet covers the top bar as well. Wider screens keep the side-by-side layout.
|
|
204
204
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
205
205
|
In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately. Escape closes the card, same as Cancel, but only while it is empty (no text, no attachment); with unsent text or an attachment present, Escape does nothing rather than risk discarding it.
|
package/dist/chrome-client.js
CHANGED
|
@@ -5,6 +5,7 @@ const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
|
|
|
5
5
|
const key = String(sessionData.key || "");
|
|
6
6
|
const filePath = String(sessionData.file || "");
|
|
7
7
|
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
8
|
+
const terminalStorageKey = "lavish-axi:terminal:" + key;
|
|
8
9
|
// Review-chrome state that must survive a browser refresh. Keyed per session so one review's
|
|
9
10
|
// triage can never leak into another artifact's.
|
|
10
11
|
const warningSelectionStorageKey = "lavish-axi:warning-selection:" + key;
|
|
@@ -183,7 +184,6 @@ const queued = loadQueuedPrompts();
|
|
|
183
184
|
let annotation = true;
|
|
184
185
|
let ended = false;
|
|
185
186
|
let agentPresence = "waiting";
|
|
186
|
-
let pendingSnapshot = "";
|
|
187
187
|
const layoutGateEnabled = sessionData.layoutGateEnabled !== false;
|
|
188
188
|
const configuredLayoutGateMaxHoldMs = Number(sessionData.layoutGateMaxHoldMs);
|
|
189
189
|
const layoutGateMaxHoldMs =
|
|
@@ -211,11 +211,24 @@ let layoutGateTimer;
|
|
|
211
211
|
let layoutWarnings = Array.isArray(sessionData.initialLayoutWarnings) ? sessionData.initialLayoutWarnings : [];
|
|
212
212
|
const selectedWarningIds = new Set(loadJsonState(warningSelectionStorageKey, []));
|
|
213
213
|
let warningsDrawerOpen = false;
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
/** @typedef {{ done: Promise<boolean>, finish: (succeeded: boolean) => void }} FeedbackPreparation */
|
|
215
|
+
/** @typedef {{ prompts: any[], inFlight: boolean, order: number }} TerminalSubmission */
|
|
216
|
+
/** @type {Map<string, { action: "copy" | "submit", prompts?: any[], endAfter?: boolean, terminal?: TerminalSubmission | null, acknowledgement?: object, order?: number, timeout?: ReturnType<typeof setTimeout> }>} */
|
|
217
|
+
const snapshotRequests = new Map();
|
|
218
|
+
let nextSnapshotRequestId = 0;
|
|
219
|
+
let nextSendOperationOrder = 0;
|
|
216
220
|
let workingBubble = null;
|
|
217
221
|
let submitQueuedPromise = null;
|
|
218
|
-
|
|
222
|
+
const pendingSubmissions = [];
|
|
223
|
+
const deliveredPrompts = new WeakSet();
|
|
224
|
+
const pendingAcknowledgements = new Set();
|
|
225
|
+
/** @type {Set<FeedbackPreparation>} */
|
|
226
|
+
const feedbackPreparations = new Set();
|
|
227
|
+
/** @type {TerminalSubmission | null} */
|
|
228
|
+
let terminalSubmission =
|
|
229
|
+
loadJsonState(terminalStorageKey, false) === true
|
|
230
|
+
? { prompts: queued.slice(), inFlight: false, order: ++nextSendOperationOrder }
|
|
231
|
+
: null;
|
|
219
232
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
220
233
|
let sendAcknowledgementTimer;
|
|
221
234
|
let lastScroll = { x: 0, y: 0 };
|
|
@@ -249,10 +262,17 @@ const HEALTH_PROBE_TIMEOUT_MS = 4000;
|
|
|
249
262
|
// batch. Bound that whole wait so a missing SDK response or a browser/network stall can never look
|
|
250
263
|
// like a dead button while the user's queue remains safely stored in this tab.
|
|
251
264
|
const SEND_ACKNOWLEDGEMENT_WARNING_MS = 10_000;
|
|
265
|
+
const TERMINAL_PREPARATION_TIMEOUT_MS = 5000;
|
|
266
|
+
// A DOM snapshot adds useful context, but the reviewer's own words are the payload.
|
|
267
|
+
// If the artifact frame navigated away from the injected SDK (or otherwise stopped
|
|
268
|
+
// answering), deliver those words without a snapshot instead of waiting forever.
|
|
269
|
+
const SNAPSHOT_REQUEST_TIMEOUT_MS = 5000;
|
|
252
270
|
const SEND_STALLED_COPY =
|
|
253
271
|
"Still trying to send. Your feedback is saved in this tab. Keep this tab open while Lavish catches up, and check that the server is running.";
|
|
254
272
|
const SEND_FAILED_COPY =
|
|
255
273
|
"Could not send. Your feedback is still queued in this tab. Check that Lavish is running, then click Send to Agent to retry.";
|
|
274
|
+
const TERMINAL_SEND_FAILED_COPY =
|
|
275
|
+
"Could not send. Your terminal feedback is still queued in this tab. Check that Lavish is running, then click Send & End to retry the same batch.";
|
|
256
276
|
const HEALTH_NO_ANSWER_TITLE = "Lavish did not answer.";
|
|
257
277
|
const HEALTH_NO_ANSWER_COPY =
|
|
258
278
|
"Lavish did not answer the check, so this page cannot tell whether it is running. Try again in a moment.";
|
|
@@ -274,6 +294,9 @@ let copyHintTimer;
|
|
|
274
294
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
275
295
|
let sendHintTimer;
|
|
276
296
|
let sendHintPersistent = false;
|
|
297
|
+
/** @type {{ kind: "preparation" | "submission" | "terminal", operation: object, preparationType?: string } | null} */
|
|
298
|
+
let sendFailureOwner = null;
|
|
299
|
+
let sendAcknowledgementWarningVisible = false;
|
|
277
300
|
|
|
278
301
|
function artifactFrameSrcForLoad(load) {
|
|
279
302
|
const separator = artifactSrc.includes("?") ? "&" : "?";
|
|
@@ -380,6 +403,15 @@ function persistQueuedPrompts() {
|
|
|
380
403
|
}
|
|
381
404
|
}
|
|
382
405
|
|
|
406
|
+
function persistTerminalReservation(reserved) {
|
|
407
|
+
try {
|
|
408
|
+
if (reserved) sessionStorage.setItem(terminalStorageKey, "true");
|
|
409
|
+
else sessionStorage.removeItem(terminalStorageKey);
|
|
410
|
+
} catch {
|
|
411
|
+
// Session storage can be unavailable; the in-memory reservation still protects this page.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
383
415
|
function promptTargetLabel(prompt) {
|
|
384
416
|
if (prompt?.target?.type === "table-cell") {
|
|
385
417
|
const semantic = [prompt.target.rowLabel, prompt.target.columnLabel].filter(Boolean).join(" → ");
|
|
@@ -423,6 +455,7 @@ function render() {
|
|
|
423
455
|
|
|
424
456
|
for (const button of annotationPills.querySelectorAll(".pill-close")) {
|
|
425
457
|
const closeButton = /** @type {HTMLButtonElement} */ (button);
|
|
458
|
+
closeButton.disabled = terminalSubmission !== null;
|
|
426
459
|
closeButton.addEventListener("click", (event) => removeQueuedPrompt(Number(closeButton.dataset.index), event));
|
|
427
460
|
}
|
|
428
461
|
updateSendState();
|
|
@@ -431,8 +464,17 @@ function render() {
|
|
|
431
464
|
}
|
|
432
465
|
|
|
433
466
|
function updateSendState() {
|
|
434
|
-
|
|
435
|
-
|
|
467
|
+
const terminalReserved = terminalSubmission !== null;
|
|
468
|
+
// A terminal send owns the exact review batch, so freeze interactions inside the
|
|
469
|
+
// artifact without disabling annotation mode. Disabling annotation mode closes the
|
|
470
|
+
// SDK card and destroys an unsent draft before delivery has actually succeeded.
|
|
471
|
+
frame.inert = ended || terminalReserved;
|
|
472
|
+
sendButton.disabled = ended || terminalReserved;
|
|
473
|
+
sendAndEndButton.disabled = ended || Boolean(terminalSubmission?.inFlight);
|
|
474
|
+
annotationSwitch.disabled = ended || terminalReserved;
|
|
475
|
+
chatInput.disabled = ended || terminalReserved;
|
|
476
|
+
chatAttachButton.disabled = ended || terminalReserved;
|
|
477
|
+
endButton.disabled = ended || terminalReserved;
|
|
436
478
|
if (warningsQueueButton) updateWarningSelectionState();
|
|
437
479
|
}
|
|
438
480
|
|
|
@@ -513,24 +555,49 @@ function hideSendHint(force = false) {
|
|
|
513
555
|
sendHint.textContent = DEFAULT_SEND_HINT;
|
|
514
556
|
sendHint.classList.remove("persistent");
|
|
515
557
|
sendHintPersistent = false;
|
|
558
|
+
sendAcknowledgementWarningVisible = false;
|
|
516
559
|
}
|
|
517
560
|
|
|
518
561
|
function armSendAcknowledgementWarning() {
|
|
519
|
-
if (sendAcknowledgementTimer ||
|
|
562
|
+
if (sendAcknowledgementTimer || pendingAcknowledgements.size === 0) return;
|
|
520
563
|
sendAcknowledgementTimer = setTimeout(() => {
|
|
521
564
|
sendAcknowledgementTimer = undefined;
|
|
522
|
-
if (
|
|
565
|
+
if (pendingAcknowledgements.size > 0 && !sendFailureOwner) {
|
|
566
|
+
sendAcknowledgementWarningVisible = true;
|
|
567
|
+
showSendHint(SEND_STALLED_COPY, null, false);
|
|
568
|
+
}
|
|
523
569
|
}, SEND_ACKNOWLEDGEMENT_WARNING_MS);
|
|
524
570
|
}
|
|
525
571
|
|
|
526
572
|
function clearSendAcknowledgementWarning() {
|
|
527
573
|
clearTimeout(sendAcknowledgementTimer);
|
|
528
574
|
sendAcknowledgementTimer = undefined;
|
|
575
|
+
sendAcknowledgementWarningVisible = false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function showQueuedSendFailure(message = SEND_FAILED_COPY, owner = null) {
|
|
579
|
+
showPersistentSendFailure(message, true, owner);
|
|
529
580
|
}
|
|
530
581
|
|
|
531
|
-
function
|
|
582
|
+
function showPersistentSendFailure(message, requireQueuedFeedback = false, owner = null) {
|
|
532
583
|
clearSendAcknowledgementWarning();
|
|
533
|
-
if (queued.length)
|
|
584
|
+
if (requireQueuedFeedback && !queued.length) return;
|
|
585
|
+
if (sendFailureOwner?.kind === "preparation") return;
|
|
586
|
+
const currentOrder = Number(sendFailureOwner?.operation?.order) || 0;
|
|
587
|
+
const nextOrder = Number(owner?.operation?.order) || 0;
|
|
588
|
+
if (owner?.kind !== "preparation" && currentOrder > nextOrder) return;
|
|
589
|
+
sendFailureOwner = owner;
|
|
590
|
+
showSendHint(message, null, false);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function clearPersistentSendFailure() {
|
|
594
|
+
sendFailureOwner = null;
|
|
595
|
+
hideSendHint(true);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function clearPreparationFailure(preparationType) {
|
|
599
|
+
if (sendFailureOwner?.kind !== "preparation" || sendFailureOwner.preparationType !== preparationType) return;
|
|
600
|
+
clearPersistentSendFailure();
|
|
534
601
|
}
|
|
535
602
|
|
|
536
603
|
function setMenuOpen(button, menu, open) {
|
|
@@ -939,11 +1006,12 @@ function scrollElementIntoView(el) {
|
|
|
939
1006
|
|
|
940
1007
|
function removeQueuedPrompt(index, event) {
|
|
941
1008
|
if (event) event.stopPropagation();
|
|
1009
|
+
if (terminalSubmission) return;
|
|
942
1010
|
queued.splice(index, 1);
|
|
943
1011
|
persistQueuedPrompts();
|
|
944
1012
|
if (!queued.length) {
|
|
945
1013
|
clearSendAcknowledgementWarning();
|
|
946
|
-
|
|
1014
|
+
if (sendFailureOwner?.kind !== "preparation") clearPersistentSendFailure();
|
|
947
1015
|
}
|
|
948
1016
|
render();
|
|
949
1017
|
}
|
|
@@ -952,9 +1020,28 @@ function promptQueueKey(prompt) {
|
|
|
952
1020
|
return prompt && typeof prompt[internalQueueKeyField] === "string" ? prompt[internalQueueKeyField].trim() : "";
|
|
953
1021
|
}
|
|
954
1022
|
|
|
955
|
-
function
|
|
1023
|
+
function beginFeedbackPreparation() {
|
|
1024
|
+
if (ended || terminalSubmission) return null;
|
|
1025
|
+
/** @type {(succeeded: boolean) => void} */
|
|
1026
|
+
let finishPromise = () => {};
|
|
1027
|
+
const done = new Promise((resolve) => {
|
|
1028
|
+
finishPromise = resolve;
|
|
1029
|
+
});
|
|
1030
|
+
const preparation = {
|
|
1031
|
+
done,
|
|
1032
|
+
finish(succeeded) {
|
|
1033
|
+
if (!feedbackPreparations.delete(preparation)) return;
|
|
1034
|
+
finishPromise(succeeded);
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
feedbackPreparations.add(preparation);
|
|
1038
|
+
return preparation;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function enqueuePrompt(rawPrompt, /** @type {FeedbackPreparation | null} */ preparation = null) {
|
|
1042
|
+
if ((preparation && !feedbackPreparations.has(preparation)) || (terminalSubmission && !preparation)) return false;
|
|
956
1043
|
const prompt = sanitizeQueuedPrompt(rawPrompt);
|
|
957
|
-
if (!prompt) return;
|
|
1044
|
+
if (!prompt) return false;
|
|
958
1045
|
|
|
959
1046
|
const queueKey = promptQueueKey(prompt);
|
|
960
1047
|
if (queueKey) {
|
|
@@ -970,6 +1057,7 @@ function enqueuePrompt(rawPrompt) {
|
|
|
970
1057
|
|
|
971
1058
|
persistQueuedPrompts();
|
|
972
1059
|
render();
|
|
1060
|
+
return true;
|
|
973
1061
|
}
|
|
974
1062
|
|
|
975
1063
|
function stripInternalPromptFields(prompt) {
|
|
@@ -983,9 +1071,46 @@ function postToFrame(message) {
|
|
|
983
1071
|
if (frame.contentWindow) frame.contentWindow.postMessage(message, "*");
|
|
984
1072
|
}
|
|
985
1073
|
|
|
986
|
-
function requestSnapshot(action) {
|
|
987
|
-
|
|
988
|
-
|
|
1074
|
+
function requestSnapshot(action, prompts = [], endAfter = false, terminal = null) {
|
|
1075
|
+
const requestId = "snapshot-" + ++nextSnapshotRequestId;
|
|
1076
|
+
const request =
|
|
1077
|
+
action === "submit"
|
|
1078
|
+
? { action, prompts, endAfter, terminal, order: terminal?.order || ++nextSendOperationOrder }
|
|
1079
|
+
: { action };
|
|
1080
|
+
snapshotRequests.set(requestId, request);
|
|
1081
|
+
if (action === "submit") {
|
|
1082
|
+
request.acknowledgement = {};
|
|
1083
|
+
pendingAcknowledgements.add(request.acknowledgement);
|
|
1084
|
+
armSendAcknowledgementWarning();
|
|
1085
|
+
request.timeout = setTimeout(() => completeSnapshotRequest(requestId, ""), SNAPSHOT_REQUEST_TIMEOUT_MS);
|
|
1086
|
+
}
|
|
1087
|
+
postToFrame({ type: "lavish:requestSnapshot", snapshot_request_id: requestId });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function takeSnapshotRequest(requestId) {
|
|
1091
|
+
if (typeof requestId !== "string" || !requestId || !snapshotRequests.has(requestId)) return null;
|
|
1092
|
+
const request = snapshotRequests.get(requestId);
|
|
1093
|
+
snapshotRequests.delete(requestId);
|
|
1094
|
+
if (request?.timeout) clearTimeout(request.timeout);
|
|
1095
|
+
return request || null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function completeSnapshotRequest(requestId, snapshot) {
|
|
1099
|
+
const request = takeSnapshotRequest(requestId);
|
|
1100
|
+
if (!request) return;
|
|
1101
|
+
if (request.action === "copy") {
|
|
1102
|
+
copyText(snapshot || "");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
submitQueued({
|
|
1107
|
+
prompts: request.prompts || [],
|
|
1108
|
+
domSnapshot: snapshot || "",
|
|
1109
|
+
endAfter: request.endAfter === true,
|
|
1110
|
+
terminal: request.terminal || null,
|
|
1111
|
+
acknowledgement: request.acknowledgement || null,
|
|
1112
|
+
order: request.order || 0,
|
|
1113
|
+
}).catch(() => {});
|
|
989
1114
|
}
|
|
990
1115
|
|
|
991
1116
|
function createChatAttachmentsController() {
|
|
@@ -1188,6 +1313,10 @@ const chatAttachmentController = createChatAttachmentsController();
|
|
|
1188
1313
|
|
|
1189
1314
|
function sendQueued(endAfter) {
|
|
1190
1315
|
if (ended) return;
|
|
1316
|
+
if (terminalSubmission) {
|
|
1317
|
+
if (endAfter && !terminalSubmission.inFlight) retryTerminalSubmission();
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1191
1320
|
closeMenus();
|
|
1192
1321
|
|
|
1193
1322
|
// A pending or failed chip holds back only the COMPOSER message (and an
|
|
@@ -1214,50 +1343,138 @@ function sendQueued(endAfter) {
|
|
|
1214
1343
|
chatAttachmentController.reset();
|
|
1215
1344
|
}
|
|
1216
1345
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1346
|
+
const shouldEnd = Boolean(endAfter && !chipsBlocked);
|
|
1347
|
+
const preparations = shouldEnd ? [...feedbackPreparations] : [];
|
|
1348
|
+
if (!queued.length && preparations.length === 0) {
|
|
1349
|
+
if (!chipsBlocked && !sendFailureOwner) showSendHint();
|
|
1219
1350
|
return;
|
|
1220
1351
|
}
|
|
1221
|
-
hideSendHint(true);
|
|
1222
|
-
|
|
1352
|
+
if (!sendFailureOwner) hideSendHint(true);
|
|
1353
|
+
if (shouldEnd) {
|
|
1354
|
+
const terminal = {
|
|
1355
|
+
prompts: [],
|
|
1356
|
+
inFlight: true,
|
|
1357
|
+
order: ++nextSendOperationOrder,
|
|
1358
|
+
};
|
|
1359
|
+
terminalSubmission = terminal;
|
|
1360
|
+
updateSendState();
|
|
1361
|
+
finishTerminalPreparation(terminal, preparations);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
requestSnapshot("submit", queued.slice(), false, null);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function finishTerminalPreparation(terminal, preparations) {
|
|
1368
|
+
if (preparations.length === 0) {
|
|
1369
|
+
completeTerminalPreparation(terminal, []);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const timeout = setTimeout(() => {
|
|
1373
|
+
if (terminalSubmission !== terminal || ended) return;
|
|
1374
|
+
for (const preparation of preparations) preparation.finish(false);
|
|
1375
|
+
showPersistentSendFailure(
|
|
1376
|
+
"Could not finish preparing all feedback within 5 seconds. This review remains open, and existing queued feedback is still editable.",
|
|
1377
|
+
false,
|
|
1378
|
+
{ kind: "preparation", operation: terminal, preparationType: "terminal" },
|
|
1379
|
+
);
|
|
1380
|
+
releaseTerminalSubmission(terminal);
|
|
1381
|
+
}, TERMINAL_PREPARATION_TIMEOUT_MS);
|
|
1382
|
+
Promise.all(preparations.map((preparation) => preparation.done)).then((results) => {
|
|
1383
|
+
clearTimeout(timeout);
|
|
1384
|
+
completeTerminalPreparation(terminal, results);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function completeTerminalPreparation(terminal, results) {
|
|
1389
|
+
if (terminalSubmission !== terminal || ended) return;
|
|
1390
|
+
if (results.some((succeeded) => !succeeded)) {
|
|
1391
|
+
releaseTerminalSubmission(terminal);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
terminal.prompts = queued.slice();
|
|
1395
|
+
clearPreparationFailure("terminal");
|
|
1396
|
+
if (!terminal.prompts.length) {
|
|
1397
|
+
releaseTerminalSubmission(terminal);
|
|
1398
|
+
if (!sendFailureOwner) showSendHint();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
// Only make the reservation durable once every preparation has joined the exact
|
|
1402
|
+
// batch. A reload before this point must restore an ordinary editable queue, not
|
|
1403
|
+
// an incomplete terminal submission.
|
|
1404
|
+
persistTerminalReservation(true);
|
|
1405
|
+
requestSnapshot("submit", terminal.prompts, true, terminal);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function retryTerminalSubmission() {
|
|
1409
|
+
if (!terminalSubmission || terminalSubmission.inFlight || ended) return;
|
|
1410
|
+
terminalSubmission.inFlight = true;
|
|
1411
|
+
updateSendState();
|
|
1412
|
+
requestSnapshot("submit", terminalSubmission.prompts, true, terminalSubmission);
|
|
1413
|
+
}
|
|
1223
1414
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1415
|
+
function markTerminalSubmissionFailed(submission) {
|
|
1416
|
+
if (!terminalSubmission || submission.terminal !== terminalSubmission || ended) return;
|
|
1417
|
+
terminalSubmission.inFlight = false;
|
|
1418
|
+
updateSendState();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function releaseTerminalSubmission(terminal) {
|
|
1422
|
+
if (terminalSubmission !== terminal || ended) return;
|
|
1423
|
+
terminalSubmission = null;
|
|
1424
|
+
persistTerminalReservation(false);
|
|
1425
|
+
render();
|
|
1226
1426
|
}
|
|
1227
1427
|
|
|
1228
|
-
async function submitQueued() {
|
|
1428
|
+
async function submitQueued(submission) {
|
|
1429
|
+
pendingSubmissions.push(submission);
|
|
1229
1430
|
if (submitQueuedPromise) {
|
|
1230
|
-
submitQueuedAgain = true;
|
|
1231
1431
|
return submitQueuedPromise;
|
|
1232
1432
|
}
|
|
1233
1433
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1434
|
+
submitQueuedPromise = (async () => {
|
|
1435
|
+
/** @type {unknown} */
|
|
1436
|
+
let firstError = null;
|
|
1437
|
+
while (pendingSubmissions.length && !ended) {
|
|
1438
|
+
const next = pendingSubmissions.shift();
|
|
1439
|
+
if (!next) continue;
|
|
1440
|
+
try {
|
|
1441
|
+
const result = await submitQueuedOnce(next, firstError !== null);
|
|
1442
|
+
if (result === false) markTerminalSubmissionFailed(next);
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
markTerminalSubmissionFailed(next);
|
|
1445
|
+
if (firstError === null) firstError = error;
|
|
1446
|
+
} finally {
|
|
1447
|
+
if (next.acknowledgement) pendingAcknowledgements.delete(next.acknowledgement);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (firstError !== null) throw firstError;
|
|
1451
|
+
})();
|
|
1236
1452
|
try {
|
|
1237
|
-
|
|
1238
|
-
succeeded = result !== false;
|
|
1239
|
-
return result;
|
|
1453
|
+
return await submitQueuedPromise;
|
|
1240
1454
|
} finally {
|
|
1241
1455
|
submitQueuedPromise = null;
|
|
1242
|
-
const shouldSubmitAgain = submitQueuedAgain;
|
|
1243
|
-
submitQueuedAgain = false;
|
|
1244
|
-
if (!succeeded) {
|
|
1245
|
-
endAfterSubmit = false;
|
|
1246
|
-
} else if (!ended && shouldSubmitAgain) {
|
|
1247
|
-
if (queued.length) {
|
|
1248
|
-
submitQueued().catch(() => {});
|
|
1249
|
-
} else if (endAfterSubmit) {
|
|
1250
|
-
endAfterSubmit = false;
|
|
1251
|
-
endSession();
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
1456
|
}
|
|
1255
1457
|
}
|
|
1256
1458
|
|
|
1257
|
-
async function submitQueuedOnce() {
|
|
1258
|
-
const prompts =
|
|
1259
|
-
const shouldEndSession =
|
|
1260
|
-
|
|
1459
|
+
async function submitQueuedOnce(submission, preserveFailureState = false) {
|
|
1460
|
+
const prompts = submission.prompts.filter((prompt) => !deliveredPrompts.has(prompt));
|
|
1461
|
+
const shouldEndSession = submission.endAfter;
|
|
1462
|
+
if (!prompts.length) {
|
|
1463
|
+
if (shouldEndSession && !ended) {
|
|
1464
|
+
try {
|
|
1465
|
+
await endSession(submission.terminal);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
showPersistentSendFailure(TERMINAL_SEND_FAILED_COPY, false, {
|
|
1468
|
+
kind: "terminal",
|
|
1469
|
+
operation: submission.terminal,
|
|
1470
|
+
});
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
settleAcknowledgementGuidance(submission, preserveFailureState);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const body = { prompts: prompts.map(stripInternalPromptFields), domSnapshot: submission.domSnapshot };
|
|
1261
1478
|
if (shouldEndSession) body.endSession = true;
|
|
1262
1479
|
let response;
|
|
1263
1480
|
try {
|
|
@@ -1266,11 +1483,33 @@ async function submitQueuedOnce() {
|
|
|
1266
1483
|
headers: { "content-type": "application/json" },
|
|
1267
1484
|
body: JSON.stringify(body),
|
|
1268
1485
|
});
|
|
1486
|
+
// The DOM snapshot is optional context and can be the difference between a
|
|
1487
|
+
// useful terminal batch and Express' request-size limit. Retry exactly once
|
|
1488
|
+
// without it; never mutate or split the user's exact feedback batch.
|
|
1489
|
+
if (response.status === 413 && body.domSnapshot) {
|
|
1490
|
+
body.domSnapshot = "";
|
|
1491
|
+
response = await fetch("/api/" + key + "/prompts", {
|
|
1492
|
+
method: "POST",
|
|
1493
|
+
headers: { "content-type": "application/json" },
|
|
1494
|
+
body: JSON.stringify(body),
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1269
1497
|
} catch (error) {
|
|
1270
|
-
showQueuedSendFailure(
|
|
1498
|
+
showQueuedSendFailure(submission.terminal ? TERMINAL_SEND_FAILED_COPY : SEND_FAILED_COPY, {
|
|
1499
|
+
kind: submission.terminal ? "terminal" : "submission",
|
|
1500
|
+
operation: submission.terminal || submission,
|
|
1501
|
+
});
|
|
1271
1502
|
throw error;
|
|
1272
1503
|
}
|
|
1273
1504
|
if (!response.ok) {
|
|
1505
|
+
if (response.status === 413) {
|
|
1506
|
+
showQueuedSendFailure(
|
|
1507
|
+
"Could not send because this feedback is too large. It is still queued. Remove some feedback or attachments, then try again.",
|
|
1508
|
+
{ kind: "submission", operation: submission },
|
|
1509
|
+
);
|
|
1510
|
+
if (submission.terminal) releaseTerminalSubmission(submission.terminal);
|
|
1511
|
+
throw new Error("queued feedback exceeds the request size limit");
|
|
1512
|
+
}
|
|
1274
1513
|
if (response.status === 409) {
|
|
1275
1514
|
const data = await response.json().catch(() => null);
|
|
1276
1515
|
// The session already ended before this batch arrived - most likely this chrome missed the
|
|
@@ -1278,16 +1517,16 @@ async function submitQueuedOnce() {
|
|
|
1278
1517
|
// for another attempt that will be refused the same way.
|
|
1279
1518
|
if (data?.status === "ended") {
|
|
1280
1519
|
clearSendAcknowledgementWarning();
|
|
1281
|
-
endAfterSubmit = false;
|
|
1282
1520
|
markSessionEnded();
|
|
1283
1521
|
return false;
|
|
1284
1522
|
}
|
|
1285
1523
|
if (Array.isArray(data?.warnings)) setLayoutWarnings(data.warnings);
|
|
1286
|
-
endAfterSubmit = false;
|
|
1287
1524
|
showQueuedSendFailure(
|
|
1288
1525
|
"Could not send because the layout issue selection changed. Your feedback is still queued. Review the current issues, then click Send to Agent to retry.",
|
|
1526
|
+
{ kind: "submission", operation: submission },
|
|
1289
1527
|
);
|
|
1290
|
-
|
|
1528
|
+
if (submission.terminal) releaseTerminalSubmission(submission.terminal);
|
|
1529
|
+
return;
|
|
1291
1530
|
}
|
|
1292
1531
|
// C4: the server persisted nothing (atomic reject) - the queue below is left
|
|
1293
1532
|
// intact because the splice only runs on success. Surface exactly what failed
|
|
@@ -1295,31 +1534,64 @@ async function submitQueuedOnce() {
|
|
|
1295
1534
|
if (response.status === 400) {
|
|
1296
1535
|
const detail = await response.json().catch(() => ({}));
|
|
1297
1536
|
if (Array.isArray(detail.rejected) && detail.rejected.length) {
|
|
1298
|
-
showQueuedSendFailure(describeAttachmentRejection(detail.rejected, detail.caps)
|
|
1537
|
+
showQueuedSendFailure(describeAttachmentRejection(detail.rejected, detail.caps), {
|
|
1538
|
+
kind: "submission",
|
|
1539
|
+
operation: submission,
|
|
1540
|
+
});
|
|
1541
|
+
if (submission.terminal) releaseTerminalSubmission(submission.terminal);
|
|
1299
1542
|
} else {
|
|
1300
|
-
showQueuedSendFailure(
|
|
1543
|
+
showQueuedSendFailure(submission.terminal ? TERMINAL_SEND_FAILED_COPY : SEND_FAILED_COPY, {
|
|
1544
|
+
kind: submission.terminal ? "terminal" : "submission",
|
|
1545
|
+
operation: submission.terminal || submission,
|
|
1546
|
+
});
|
|
1301
1547
|
}
|
|
1302
1548
|
} else {
|
|
1303
|
-
showQueuedSendFailure(
|
|
1549
|
+
showQueuedSendFailure(submission.terminal ? TERMINAL_SEND_FAILED_COPY : SEND_FAILED_COPY, {
|
|
1550
|
+
kind: submission.terminal ? "terminal" : "submission",
|
|
1551
|
+
operation: submission.terminal || submission,
|
|
1552
|
+
});
|
|
1304
1553
|
}
|
|
1305
1554
|
throw new Error("failed to submit queued prompts");
|
|
1306
1555
|
}
|
|
1307
1556
|
for (const prompt of prompts) {
|
|
1557
|
+
deliveredPrompts.add(prompt);
|
|
1308
1558
|
const index = queued.indexOf(prompt);
|
|
1309
1559
|
if (index !== -1) queued.splice(index, 1);
|
|
1310
1560
|
}
|
|
1311
1561
|
persistQueuedPrompts();
|
|
1312
1562
|
render();
|
|
1313
|
-
|
|
1314
|
-
hideSendHint(true);
|
|
1315
|
-
if (queued.length) armSendAcknowledgementWarning();
|
|
1563
|
+
settleAcknowledgementGuidance(submission, preserveFailureState);
|
|
1316
1564
|
if (shouldEndSession) {
|
|
1317
|
-
endAfterSubmit = false;
|
|
1318
1565
|
markSessionEnded();
|
|
1319
1566
|
return;
|
|
1320
1567
|
}
|
|
1321
1568
|
}
|
|
1322
1569
|
|
|
1570
|
+
function submissionResolvesSendFailure(submission) {
|
|
1571
|
+
if (!sendFailureOwner) return true;
|
|
1572
|
+
if (sendFailureOwner.kind === "preparation") return false;
|
|
1573
|
+
if (sendFailureOwner.kind === "terminal") return sendFailureOwner.operation === submission.terminal;
|
|
1574
|
+
const failedSubmission = sendFailureOwner.operation;
|
|
1575
|
+
return (
|
|
1576
|
+
failedSubmission === submission ||
|
|
1577
|
+
(Array.isArray(failedSubmission.prompts) &&
|
|
1578
|
+
failedSubmission.prompts.every((prompt) => deliveredPrompts.has(prompt)))
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function settleAcknowledgementGuidance(submission, preserveFailureState) {
|
|
1583
|
+
if (!submissionResolvesSendFailure(submission) || (preserveFailureState && queued.length)) return;
|
|
1584
|
+
const hasLaterAcknowledgement = [...pendingAcknowledgements].some(
|
|
1585
|
+
(acknowledgement) => acknowledgement !== submission.acknowledgement,
|
|
1586
|
+
);
|
|
1587
|
+
if (hasLaterAcknowledgement) {
|
|
1588
|
+
if (!sendAcknowledgementTimer && !sendAcknowledgementWarningVisible) armSendAcknowledgementWarning();
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
clearSendAcknowledgementWarning();
|
|
1592
|
+
clearPersistentSendFailure();
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1323
1595
|
function normalizeLayoutFindings(value) {
|
|
1324
1596
|
return Array.isArray(value)
|
|
1325
1597
|
? value.filter((item) => item && typeof item === "object" && String(item.severity || "").toLowerCase() === "error")
|
|
@@ -1758,7 +2030,7 @@ function updateWarningSelectionState() {
|
|
|
1758
2030
|
warningsSelectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
|
1759
2031
|
warningsSelectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
|
1760
2032
|
warningsSelected.textContent = selectedCount === 0 ? "None selected" : selectedCount + " selected";
|
|
1761
|
-
warningsQueueButton.disabled = selectedCount === 0 || ended;
|
|
2033
|
+
warningsQueueButton.disabled = selectedCount === 0 || ended || terminalSubmission !== null;
|
|
1762
2034
|
}
|
|
1763
2035
|
|
|
1764
2036
|
function toggleSelectAllWarnings() {
|
|
@@ -1815,10 +2087,15 @@ async function dismissWarning(id) {
|
|
|
1815
2087
|
// One queued batch = one ordinary queued prompt. The CLI cannot tell it apart from any other
|
|
1816
2088
|
// feedback, which is exactly the point: no parallel agent protocol.
|
|
1817
2089
|
async function queueSelectedWarningFixes() {
|
|
1818
|
-
|
|
2090
|
+
const preparation = beginFeedbackPreparation();
|
|
2091
|
+
if (!preparation) return;
|
|
1819
2092
|
const ids = [...selectedWarningIds];
|
|
1820
|
-
if (ids.length === 0)
|
|
2093
|
+
if (ids.length === 0) {
|
|
2094
|
+
preparation.finish(true);
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
1821
2097
|
warningsQueueButton.disabled = true;
|
|
2098
|
+
let succeeded = false;
|
|
1822
2099
|
try {
|
|
1823
2100
|
const response = await fetch("/api/" + key + "/layout-warnings/queue", {
|
|
1824
2101
|
method: "POST",
|
|
@@ -1828,21 +2105,36 @@ async function queueSelectedWarningFixes() {
|
|
|
1828
2105
|
if (!response.ok) throw new Error("failed to queue layout warning fixes");
|
|
1829
2106
|
const data = await response.json();
|
|
1830
2107
|
if (data.prompt) {
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2108
|
+
if (
|
|
2109
|
+
!enqueuePrompt(
|
|
2110
|
+
{
|
|
2111
|
+
uid: "",
|
|
2112
|
+
prompt: data.prompt.prompt,
|
|
2113
|
+
selector: "",
|
|
2114
|
+
tag: "layout-warnings",
|
|
2115
|
+
text: data.prompt.text,
|
|
2116
|
+
target: data.prompt.target,
|
|
2117
|
+
},
|
|
2118
|
+
preparation,
|
|
2119
|
+
)
|
|
2120
|
+
)
|
|
2121
|
+
throw new Error("failed to retain layout warning fixes");
|
|
1839
2122
|
}
|
|
1840
2123
|
selectedWarningIds.clear();
|
|
1841
2124
|
persistWarningSelection();
|
|
1842
2125
|
if (Array.isArray(data.warnings)) setLayoutWarnings(data.warnings);
|
|
1843
2126
|
closeWarningsDrawer({ restoreFocus: true });
|
|
2127
|
+
clearPreparationFailure("layout-warnings");
|
|
2128
|
+
succeeded = true;
|
|
1844
2129
|
} catch {
|
|
2130
|
+
showPersistentSendFailure(
|
|
2131
|
+
"Could not prepare the selected layout fixes. Review the current issues and try again.",
|
|
2132
|
+
false,
|
|
2133
|
+
{ kind: "preparation", operation: preparation, preparationType: "layout-warnings" },
|
|
2134
|
+
);
|
|
1845
2135
|
updateWarningSelectionState();
|
|
2136
|
+
} finally {
|
|
2137
|
+
preparation.finish(succeeded);
|
|
1846
2138
|
}
|
|
1847
2139
|
}
|
|
1848
2140
|
|
|
@@ -1857,8 +2149,8 @@ async function refreshLayoutWarnings() {
|
|
|
1857
2149
|
}
|
|
1858
2150
|
}
|
|
1859
2151
|
|
|
1860
|
-
async function endSession() {
|
|
1861
|
-
if (ended) return;
|
|
2152
|
+
async function endSession(terminal = null) {
|
|
2153
|
+
if (ended || (terminalSubmission && terminal !== terminalSubmission)) return;
|
|
1862
2154
|
const response = await fetch("/api/" + key + "/end", { method: "POST" });
|
|
1863
2155
|
if (!response.ok) throw new Error("failed to end session");
|
|
1864
2156
|
markSessionEnded();
|
|
@@ -1867,6 +2159,10 @@ async function endSession() {
|
|
|
1867
2159
|
function markSessionEnded() {
|
|
1868
2160
|
if (ended) return;
|
|
1869
2161
|
ended = true;
|
|
2162
|
+
pendingAcknowledgements.clear();
|
|
2163
|
+
clearSendAcknowledgementWarning();
|
|
2164
|
+
terminalSubmission = null;
|
|
2165
|
+
persistTerminalReservation(false);
|
|
1870
2166
|
cancelArtifactLoadRecovery();
|
|
1871
2167
|
closeMenus();
|
|
1872
2168
|
closeShareDialog();
|
|
@@ -2619,7 +2915,17 @@ function whiteboardSummaryText(summaryLines) {
|
|
|
2619
2915
|
}
|
|
2620
2916
|
|
|
2621
2917
|
async function queueWhiteboardFeedback(index, message, mode) {
|
|
2918
|
+
const preparation = beginFeedbackPreparation();
|
|
2919
|
+
if (!preparation) {
|
|
2920
|
+
postToWhiteboard(index, mode, {
|
|
2921
|
+
type: "lavish-whiteboard:queueResult",
|
|
2922
|
+
ok: false,
|
|
2923
|
+
error: "Feedback delivery is already ending this review.",
|
|
2924
|
+
});
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2622
2927
|
const diagramId = whiteboardRecord(index).diagramId;
|
|
2928
|
+
let succeeded = false;
|
|
2623
2929
|
try {
|
|
2624
2930
|
// Persist the exact reviewed state before queueing, so the paths in the
|
|
2625
2931
|
// prompt point at what the user actually saw.
|
|
@@ -2643,36 +2949,46 @@ async function queueWhiteboardFeedback(index, message, mode) {
|
|
|
2643
2949
|
"\n\nEdited scene JSON: " +
|
|
2644
2950
|
String(files.scene_path || "") +
|
|
2645
2951
|
(files.preview_path ? "\nPNG preview: " + String(files.preview_path) : "");
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2952
|
+
if (
|
|
2953
|
+
!enqueuePrompt(
|
|
2954
|
+
{
|
|
2955
|
+
uid: "",
|
|
2956
|
+
prompt: promptText,
|
|
2957
|
+
selector: "",
|
|
2958
|
+
tag: "whiteboard",
|
|
2959
|
+
text: "Whiteboard: diagram " + (index + 1),
|
|
2960
|
+
target: {
|
|
2961
|
+
type: "excalidraw-scene",
|
|
2962
|
+
diagramIndex: index,
|
|
2963
|
+
diagramId,
|
|
2964
|
+
sourceHash: String(message.sourceHash || ""),
|
|
2965
|
+
scenePath: String(files.scene_path || ""),
|
|
2966
|
+
previewPath: String(files.preview_path || ""),
|
|
2967
|
+
imageFallback: Boolean(message.imageFallback),
|
|
2968
|
+
stats: message.stats && typeof message.stats === "object" ? message.stats : {},
|
|
2969
|
+
},
|
|
2970
|
+
// Re-queueing the same diagram's whiteboard before sending replaces the
|
|
2971
|
+
// earlier unsent prompt instead of stacking duplicates.
|
|
2972
|
+
[internalQueueKeyField]: "whiteboard:" + index,
|
|
2973
|
+
},
|
|
2974
|
+
preparation,
|
|
2975
|
+
)
|
|
2976
|
+
)
|
|
2977
|
+
throw new Error("failed to retain whiteboard feedback");
|
|
2666
2978
|
// Queued from the whiteboard inside the artifact, like any other in-artifact prompt.
|
|
2667
2979
|
pulseSheetDock();
|
|
2668
2980
|
postToWhiteboard(index, mode, { type: "lavish-whiteboard:queueResult", ok: true });
|
|
2669
2981
|
if (mode === "overlay") closeWhiteboard();
|
|
2982
|
+
clearPreparationFailure("whiteboard");
|
|
2983
|
+
succeeded = true;
|
|
2670
2984
|
} catch (error) {
|
|
2671
2985
|
postToWhiteboard(index, mode, {
|
|
2672
2986
|
type: "lavish-whiteboard:queueResult",
|
|
2673
2987
|
ok: false,
|
|
2674
2988
|
error: error instanceof Error ? error.message : String(error),
|
|
2675
2989
|
});
|
|
2990
|
+
} finally {
|
|
2991
|
+
preparation.finish(succeeded);
|
|
2676
2992
|
}
|
|
2677
2993
|
}
|
|
2678
2994
|
|
|
@@ -2987,16 +3303,7 @@ window.addEventListener("message", (event) => {
|
|
|
2987
3303
|
pulseSheetDock();
|
|
2988
3304
|
}
|
|
2989
3305
|
if (msg.type === "lavish:snapshot") {
|
|
2990
|
-
|
|
2991
|
-
if (snapshotAction === "copy") {
|
|
2992
|
-
copyText(msg.snapshot || "");
|
|
2993
|
-
} else {
|
|
2994
|
-
pendingSnapshot = msg.snapshot || "";
|
|
2995
|
-
// submitQueuedOnce throws to signal "nothing was delivered" - its own
|
|
2996
|
-
// finally already reset the end intent and it left the queue intact for a
|
|
2997
|
-
// retry, so the rejection has no remaining consumer here.
|
|
2998
|
-
submitQueued().catch(() => {});
|
|
2999
|
-
}
|
|
3306
|
+
completeSnapshotRequest(msg.snapshot_request_id, msg.snapshot || "");
|
|
3000
3307
|
}
|
|
3001
3308
|
if (msg.type === "lavish:scroll") {
|
|
3002
3309
|
lastScroll = { x: Number(msg.x) || 0, y: Number(msg.y) || 0 };
|
|
@@ -3150,7 +3457,7 @@ async function uploadAttachment(message, reportResult = postToFrame, signal) {
|
|
|
3150
3457
|
loadFrame();
|
|
3151
3458
|
|
|
3152
3459
|
function toggleAnnotationMode() {
|
|
3153
|
-
if (ended) return;
|
|
3460
|
+
if (ended || terminalSubmission) return;
|
|
3154
3461
|
annotation = !annotation;
|
|
3155
3462
|
annotationSwitch.setAttribute("aria-pressed", String(annotation));
|
|
3156
3463
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
|
package/dist/cli.mjs
CHANGED
|
@@ -6689,7 +6689,10 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
6689
6689
|
activeAttachments?.handleResult(msg.localId, msg.ok, msg.id, msg.error);
|
|
6690
6690
|
}
|
|
6691
6691
|
if (msg.type === "lavish:requestSnapshot") {
|
|
6692
|
-
postArtifactMessage("lavish:snapshot", {
|
|
6692
|
+
postArtifactMessage("lavish:snapshot", {
|
|
6693
|
+
snapshot: snapshot(),
|
|
6694
|
+
snapshot_request_id: typeof msg.snapshot_request_id === "string" ? msg.snapshot_request_id : ""
|
|
6695
|
+
});
|
|
6693
6696
|
}
|
|
6694
6697
|
if (msg.type === "lavish:restoreScroll") {
|
|
6695
6698
|
window.scrollTo(Number(msg.x) || 0, Number(msg.y) || 0);
|
|
@@ -10954,7 +10957,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
|
|
|
10954
10957
|
]);
|
|
10955
10958
|
var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback is still delivered once. After that response, polling stops, and the agent must not reopen the session uninvited.";
|
|
10956
10959
|
var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
|
|
10957
|
-
var VERSION = "0.1.
|
|
10960
|
+
var VERSION = "0.1.68";
|
|
10958
10961
|
function detectInvokingAgent(env = process.env) {
|
|
10959
10962
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
10960
10963
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "lavish-axi",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.68",
|
|
5
5
|
"description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kun Chen",
|