lavish-axi 0.1.66 → 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 +4 -3
- package/dist/chrome-client.js +407 -100
- package/dist/cli.mjs +62 -15
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -198,13 +198,14 @@ 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.
|
|
206
206
|
Cmd+I or Ctrl+I toggles between annotate and explore mode from either the browser chrome or the artifact iframe, including while focus is in a textarea or control.
|
|
207
207
|
- **Agent presence** - The browser shows when no agent is listening, keeps queued feedback for the next successful `lavish-axi poll` send even across reloads, and keeps human feedback actions available while the agent is working because the server queues them for the next poll. The agent's reply (`--agent-reply`) concludes delivered work and returns presence to waiting.
|
|
208
|
+
Closing the last review tab while a poll is active starts a 10-second reconnect grace period. A reload or quick reconnect keeps an active poll waiting; if every review stays disconnected, the poll returns `browser_disconnected` without ending the resumable session, and the agent asks whether to reopen or end it instead of acting uninvited.
|
|
208
209
|
The no-timeout poll always writes an immediate stderr banner so it is visibly not hung; it adds the periodic stderr wait ticks only in an interactive terminal, so when stderr is piped (as under agent harnesses) the captured output carries no tick noise. Stdout always stays reserved for the final response; if the poll is interrupted or times out before feedback arrives, re-run it because feedback remains queued until delivery. Poll delivery consumes the response, so read the complete response before truncating or filtering it.
|
|
209
210
|
Codex-specific guidance keeps that poll attached to the active turn instead of hiding it in a background task, because completed background tasks may not resume the agent.
|
|
210
211
|
- **Session end etiquette** - Lavish tracks who ended a session: a human clicking **End session** (or **Send & end session**) in the browser is a user-initiated end, while `lavish-axi end <html-file>` is agent-initiated.
|
|
@@ -253,7 +254,7 @@ pnpm link
|
|
|
253
254
|
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
254
255
|
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
255
256
|
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Unresolved layout issues from earlier in the session are preserved. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
|
|
256
|
-
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback
|
|
257
|
+
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or leaves every review disconnected past the reconnect grace period; detected layout issues arrive only when queued. On `browser_disconnected`, ask before reopening or ending the still-resumable session. On `ended`, stop polling and do not reopen uninvited. |
|
|
257
258
|
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later. |
|
|
258
259
|
| `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links. |
|
|
259
260
|
| `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default, `--private` locks one behind a generated password, and the same command republishes or unpublishes an existing page with `--site`/`--update-key`. |
|
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);
|
|
@@ -8813,6 +8816,7 @@ var WHITEBOARD_CHANNEL_TOKEN_TTL_MS = 5 * 6e4;
|
|
|
8813
8816
|
var NETWORK_RECONCILE_CACHE_MS = 1e3;
|
|
8814
8817
|
var TAILSCALE_BIND_RETRY_DELAYS_MS = [100, 250, 500];
|
|
8815
8818
|
var WEBSOCKET_CLOSE_GRACE_MS = 250;
|
|
8819
|
+
var BROWSER_DISCONNECT_GRACE_MS = 1e4;
|
|
8816
8820
|
var ARTIFACT_CONTENT_SECURITY_POLICY = "sandbox allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads";
|
|
8817
8821
|
var ATTACHMENT_SWEEP_INTERVAL_MS = 60 * 6e4;
|
|
8818
8822
|
var SHUTDOWN_REASONS = /* @__PURE__ */ new Set(["upgrade", "local-build", "stop"]);
|
|
@@ -8910,6 +8914,7 @@ async function serve({
|
|
|
8910
8914
|
debug = false,
|
|
8911
8915
|
log = null,
|
|
8912
8916
|
pollHeartbeatMs = 15e3,
|
|
8917
|
+
browserDisconnectGraceMs = BROWSER_DISCONNECT_GRACE_MS,
|
|
8913
8918
|
idleTimeoutMs = resolveIdleTimeoutMs(),
|
|
8914
8919
|
host = bindHost(env),
|
|
8915
8920
|
hosts,
|
|
@@ -8942,6 +8947,7 @@ async function serve({
|
|
|
8942
8947
|
const activePolls = /* @__PURE__ */ new Map();
|
|
8943
8948
|
const deliveredFeedback = /* @__PURE__ */ new Set();
|
|
8944
8949
|
const liveEventClients = /* @__PURE__ */ new Map();
|
|
8950
|
+
const browserDisconnectTimers = /* @__PURE__ */ new Map();
|
|
8945
8951
|
const whiteboardChannelSecret = crypto6.randomBytes(32);
|
|
8946
8952
|
const outstandingRepairBatches = /* @__PURE__ */ new Set();
|
|
8947
8953
|
const diagnosticViewportClasses = resolveDiagnosticViewportClasses();
|
|
@@ -8954,6 +8960,7 @@ async function serve({
|
|
|
8954
8960
|
let serverReady = false;
|
|
8955
8961
|
let networkReconcileCheckedAt = 0;
|
|
8956
8962
|
let cachedNetworkStale = false;
|
|
8963
|
+
let shuttingDown = false;
|
|
8957
8964
|
let networkReconcilePromise = null;
|
|
8958
8965
|
function broadcastLiveEvent(type, key, data = {}) {
|
|
8959
8966
|
for (const [client, clientKey] of liveEventClients) {
|
|
@@ -8965,7 +8972,30 @@ async function serve({
|
|
|
8965
8972
|
events.on("agent-presence", (key, state) => broadcastLiveEvent("agent-presence", key, { state }));
|
|
8966
8973
|
events.on("layout-warnings", (key, warnings) => broadcastLiveEvent("layout-warnings", key, { warnings }));
|
|
8967
8974
|
events.on("ended", (key, endedBy) => broadcastLiveEvent("ended", key, { ended_by: endedBy || null }));
|
|
8975
|
+
function hasLiveEventClient(key) {
|
|
8976
|
+
for (const clientKey of liveEventClients.values()) {
|
|
8977
|
+
if (clientKey === key) return true;
|
|
8978
|
+
}
|
|
8979
|
+
return false;
|
|
8980
|
+
}
|
|
8981
|
+
function clearBrowserDisconnectTimer(key) {
|
|
8982
|
+
const timer = browserDisconnectTimers.get(key);
|
|
8983
|
+
if (!timer) return;
|
|
8984
|
+
clearTimeout(timer);
|
|
8985
|
+
browserDisconnectTimers.delete(key);
|
|
8986
|
+
}
|
|
8987
|
+
function scheduleBrowserDisconnect(key) {
|
|
8988
|
+
clearBrowserDisconnectTimer(key);
|
|
8989
|
+
if (shuttingDown || hasLiveEventClient(key) || !activePolls.has(key)) return;
|
|
8990
|
+
const timer = setTimeout(() => {
|
|
8991
|
+
browserDisconnectTimers.delete(key);
|
|
8992
|
+
if (!hasLiveEventClient(key) && activePolls.has(key)) events.emit("browser-disconnected", key);
|
|
8993
|
+
}, browserDisconnectGraceMs);
|
|
8994
|
+
timer.unref?.();
|
|
8995
|
+
browserDisconnectTimers.set(key, timer);
|
|
8996
|
+
}
|
|
8968
8997
|
function attachLiveEventClient(client, key, onClose) {
|
|
8998
|
+
clearBrowserDisconnectTimer(key);
|
|
8969
8999
|
liveEventClients.set(client, key);
|
|
8970
9000
|
refreshIdleTimer();
|
|
8971
9001
|
let cleanedUp = false;
|
|
@@ -8973,6 +9003,7 @@ async function serve({
|
|
|
8973
9003
|
if (cleanedUp) return;
|
|
8974
9004
|
cleanedUp = true;
|
|
8975
9005
|
liveEventClients.delete(client);
|
|
9006
|
+
scheduleBrowserDisconnect(key);
|
|
8976
9007
|
refreshIdleTimer();
|
|
8977
9008
|
};
|
|
8978
9009
|
onClose(cleanup);
|
|
@@ -9252,25 +9283,28 @@ async function serve({
|
|
|
9252
9283
|
if (heartbeat) clearInterval(heartbeat);
|
|
9253
9284
|
events.off("feedback", onFeedback);
|
|
9254
9285
|
events.off("ended", onFeedback);
|
|
9286
|
+
events.off("browser-disconnected", onBrowserDisconnected);
|
|
9255
9287
|
setPollActive(key, activePolls, deliveredFeedback, events, false);
|
|
9288
|
+
if (!activePolls.has(key)) clearBrowserDisconnectTimer(key);
|
|
9256
9289
|
refreshIdleTimer();
|
|
9257
9290
|
cleanupPoll = null;
|
|
9258
9291
|
detachRequestClose();
|
|
9259
9292
|
};
|
|
9260
|
-
const respond = async () => {
|
|
9293
|
+
const respond = async (forcedResult = null) => {
|
|
9261
9294
|
if (responding || res.writableEnded) return;
|
|
9262
9295
|
responding = true;
|
|
9263
9296
|
try {
|
|
9264
9297
|
const result = await store.takeFeedback(key);
|
|
9298
|
+
const responseResult = forcedResult && result.status === "waiting" ? forcedResult : result;
|
|
9265
9299
|
if (requestClosed || req.destroyed || res.writableEnded) {
|
|
9266
|
-
await restoreClosedFeedback(key,
|
|
9300
|
+
await restoreClosedFeedback(key, responseResult);
|
|
9267
9301
|
return;
|
|
9268
9302
|
}
|
|
9269
|
-
finishFeedbackDelivery(key,
|
|
9303
|
+
finishFeedbackDelivery(key, responseResult);
|
|
9270
9304
|
if (streamHeartbeat) {
|
|
9271
|
-
res.end(JSON.stringify(
|
|
9305
|
+
res.end(JSON.stringify(responseResult));
|
|
9272
9306
|
} else {
|
|
9273
|
-
res.json(
|
|
9307
|
+
res.json(responseResult);
|
|
9274
9308
|
}
|
|
9275
9309
|
} finally {
|
|
9276
9310
|
cleanup();
|
|
@@ -9282,8 +9316,13 @@ async function serve({
|
|
|
9282
9316
|
}
|
|
9283
9317
|
respond().catch(handleRespondError);
|
|
9284
9318
|
};
|
|
9319
|
+
const onBrowserDisconnected = (changedKey) => {
|
|
9320
|
+
if (changedKey !== key || res.writableEnded) return;
|
|
9321
|
+
respond({ status: "browser_disconnected" }).catch(handleRespondError);
|
|
9322
|
+
};
|
|
9285
9323
|
events.on("feedback", onFeedback);
|
|
9286
9324
|
events.on("ended", onFeedback);
|
|
9325
|
+
events.on("browser-disconnected", onBrowserDisconnected);
|
|
9287
9326
|
cleanupPoll = cleanup;
|
|
9288
9327
|
if (requestClosed || req.destroyed || res.writableEnded) {
|
|
9289
9328
|
cleanup();
|
|
@@ -10045,7 +10084,6 @@ ${body}`
|
|
|
10045
10084
|
}
|
|
10046
10085
|
publicPort = httpServers[0].address().port;
|
|
10047
10086
|
serverReady = true;
|
|
10048
|
-
let shuttingDown = false;
|
|
10049
10087
|
function shutdown(reloadKey = "", reason = "") {
|
|
10050
10088
|
if (shuttingDown) return;
|
|
10051
10089
|
shuttingDown = true;
|
|
@@ -10070,6 +10108,8 @@ ${body}`
|
|
|
10070
10108
|
}
|
|
10071
10109
|
}
|
|
10072
10110
|
liveEventClients.clear();
|
|
10111
|
+
for (const timer of browserDisconnectTimers.values()) clearTimeout(timer);
|
|
10112
|
+
browserDisconnectTimers.clear();
|
|
10073
10113
|
for (const w of watchers.values()) {
|
|
10074
10114
|
w.close().catch(() => {
|
|
10075
10115
|
});
|
|
@@ -10912,11 +10952,12 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
|
|
|
10912
10952
|
"Never use `nohup`, shell `&`, `disown`, redirected fire-and-forget processes, or a detached terminal without an explicit verified callback merely to keep polling alive.",
|
|
10913
10953
|
"If the harness has no completion-aware background facility, use the foreground poll or first wire a verified wake callback into the surrounding supervisor.",
|
|
10914
10954
|
"Do not tell the user the artifact is being monitored until that wake path is live.",
|
|
10915
|
-
"If the poll gets killed or times out before feedback arrives, re-run it - feedback remains queued until delivery. Poll delivery consumes the response, so read it completely."
|
|
10955
|
+
"If the poll gets killed or times out before feedback arrives, re-run it - feedback remains queued until delivery. Poll delivery consumes the response, so read it completely.",
|
|
10956
|
+
"If it returns browser_disconnected, the review window stayed disconnected past its reconnect grace period but the session remains resumable; ask the user whether to reopen or end it, and do neither uninvited."
|
|
10916
10957
|
]);
|
|
10917
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.";
|
|
10918
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.";
|
|
10919
|
-
var VERSION = "0.1.
|
|
10960
|
+
var VERSION = "0.1.68";
|
|
10920
10961
|
function detectInvokingAgent(env = process.env) {
|
|
10921
10962
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
10922
10963
|
}
|
|
@@ -11032,7 +11073,7 @@ function createHomeOutput({ bin, sessions, includeSessions = true, agent = "gene
|
|
|
11032
11073
|
"Run `lavish-axi <html-file>` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention",
|
|
11033
11074
|
"Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
|
|
11034
11075
|
"Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work",
|
|
11035
|
-
`Run \`lavish-axi poll <html-file>\` to wait for user feedback. It long-polls and stays silent until the user sends feedback
|
|
11076
|
+
`Run \`lavish-axi poll <html-file>\` to wait for user feedback. It long-polls and stays silent until the user sends feedback, ends the session, or leaves every review window disconnected past the reconnect grace period, so leave it running - never kill it. Detected layout issues never return this poll: the browser files them in the user's Layout issues inbox in the Lavish top bar, and they arrive as an ordinary tag "layout-warnings" prompt only when the user selects them and queues the fixes. Never edit the artifact to chase a layout issue the user has not queued. The only exception is a fatal artifact_failures response, which means the review surface itself could not be used. ${pollExecutionGuidance({ agent })} ${POLL_SEND_AND_END_RULE}`,
|
|
11036
11077
|
'Mermaid is the whiteboard opt-in, not the diagram default: only when the user asks for an editable whiteboard, author that diagram as Mermaid in a `.mermaid` container. Rendered Mermaid diagrams there become embedded, editable Excalidraw whiteboards in the browser (click a diagram to unlock editing; a Fullscreen action opens it over the whole viewport) - flowchart, sequence, class, ER, and state diagrams convert to editable shapes; other types embed as an image to draw on. Scenes autosave locally; an unmodified autosave silently re-converts when a reload changes the Mermaid source. If the reviewer edited the scene, they choose to re-convert and discard saved edits or keep editing the saved scene. Standalone and exported copies still render plain Mermaid. Queue feedback adds a prompt to the Conversation panel; when the user sends it, poll returns a tag "whiteboard" prompt carrying a bounded edit summary plus local scenePath (.excalidraw JSON) and previewPath (PNG) files - read the summary first, open the files only when needed, then apply the edits by updating the Mermaid source in the artifact (never try to write the scene back)',
|
|
11037
11078
|
"Run `lavish-axi end <html-file>` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `lavish-axi <html-file>` refuses to reopen it without `--reopen`",
|
|
11038
11079
|
"Run `lavish-axi export <html-file> [--out <path>]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu",
|
|
@@ -11073,7 +11114,7 @@ function createOpenOutput({
|
|
|
11073
11114
|
session: { file, url, status },
|
|
11074
11115
|
...networkWarning ? { network_warning: networkWarning } : {},
|
|
11075
11116
|
...selfPaintWarning ? { self_paint_warning: selfPaintWarning } : {},
|
|
11076
|
-
next_step: `${selfPaintPrefix}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback
|
|
11117
|
+
next_step: `${selfPaintPrefix}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback, ends the session, or leaves every review window disconnected past the reconnect grace period, and it stays silent the whole time - that is normal, never kill it. Layout issues the browser detects do not return this poll; they wait in the user's Layout issues inbox until the user queues them, then arrive as an ordinary tag "layout-warnings" prompt. Do not pass --timeout-ms during normal agent use. ${pollExecutionGuidance({ agent })} After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show your response in Lavish Editor and wait for more feedback. If the user ends the session, stop polling and do not reopen it by re-running \`lavish-axi ${file}\` unless the user asks for further review or something genuinely important needs their visual attention - deliver routine updates directly in this conversation instead. When reopening is warranted, run \`lavish-axi ${file} --reopen\`.`
|
|
11077
11118
|
};
|
|
11078
11119
|
}
|
|
11079
11120
|
function createUserEndedOpenOutput({ file, url, networkWarning = void 0 }) {
|
|
@@ -11174,11 +11215,11 @@ ${pollInterruptedText(absolute)}
|
|
|
11174
11215
|
}
|
|
11175
11216
|
}
|
|
11176
11217
|
function pollWaitBannerText(file) {
|
|
11177
|
-
return `[lavish-axi] Long-polling for user feedback on ${file}. This stays silent until the user sends feedback
|
|
11218
|
+
return `[lavish-axi] Long-polling for user feedback on ${file}. This stays silent until the user sends feedback, ends the session, or leaves every review window disconnected past the reconnect grace period - leave it running. Detected layout issues do NOT return this poll: they wait in the user's Layout issues inbox until the user queues them as ordinary feedback. If it gets killed or times out before feedback arrives, re-run \`lavish-axi poll ${file}\` - feedback remains queued until delivery. Poll delivery consumes the response, so read it completely.`;
|
|
11178
11219
|
}
|
|
11179
11220
|
function pollWaitTickText(elapsedMs) {
|
|
11180
11221
|
const minutes = Math.round(elapsedMs / 6e4);
|
|
11181
|
-
return `[lavish-axi] Still waiting for user feedback (${minutes}m). Leave this running until the user sends feedback
|
|
11222
|
+
return `[lavish-axi] Still waiting for user feedback (${minutes}m). Leave this running until the user sends feedback, ends the session, or leaves every review window disconnected.`;
|
|
11182
11223
|
}
|
|
11183
11224
|
function pollInterruptedText(file) {
|
|
11184
11225
|
return `[lavish-axi] Poll interrupted before user feedback arrived. The user may still be reviewing - re-run \`lavish-axi poll ${file}\` to keep waiting; feedback remains queued until delivery. Poll delivery consumes the response, so read it completely.`;
|
|
@@ -11232,6 +11273,12 @@ function createPollOutput({ file, response, agent = "generic" }) {
|
|
|
11232
11273
|
next_step: createEndedNextStep(file, response.ended_by)
|
|
11233
11274
|
};
|
|
11234
11275
|
}
|
|
11276
|
+
if (response.status === "browser_disconnected") {
|
|
11277
|
+
return {
|
|
11278
|
+
session: { file, status: "browser_disconnected" },
|
|
11279
|
+
next_step: "The Lavish review window was closed or disconnected. The session remains open and resumable; ask the user whether they want to reopen it or end the session. Do not reopen or end it without their direction."
|
|
11280
|
+
};
|
|
11281
|
+
}
|
|
11235
11282
|
return {
|
|
11236
11283
|
session: { file, status: response.status || "waiting" },
|
|
11237
11284
|
next_step: `No user feedback arrived before the optional timeout. Run \`lavish-axi poll ${file}\` without --timeout-ms to wait indefinitely - feedback remains queued until delivery, so re-running the poll is safe while waiting. Poll delivery consumes the response, so read it completely.`
|
|
@@ -11251,7 +11298,7 @@ function createFeedbackNextStep(file, artifactFailures, sessionEnded, endedBy, p
|
|
|
11251
11298
|
return `${failureNote}${layoutNote}${whiteboardNote}${attachmentNote}This was the last feedback before the Lavish Editor session ended. Stop polling ${file}. Deliver any remaining updates directly in this conversation, or run \`lavish-axi ${file}\` to open a fresh session if the user needs further visual review.`;
|
|
11252
11299
|
}
|
|
11253
11300
|
const prefix = count > 0 ? artifactFailuresPrefix(file, artifactFailures) : `Apply the requested changes to ${file}. `;
|
|
11254
|
-
return `${prefix}${layoutNote}${whiteboardNote}${attachmentNote}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms unless the user ended the session. The poll waits silently until the user sends more feedback
|
|
11301
|
+
return `${prefix}${layoutNote}${whiteboardNote}${attachmentNote}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms unless the user ended the session. The poll waits silently until the user sends more feedback, ends the session, or leaves every review window disconnected past the reconnect grace period - never kill it. ${pollExecutionGuidance({ agent })}`;
|
|
11255
11302
|
}
|
|
11256
11303
|
function artifactFailuresPrefix(file, artifactFailures) {
|
|
11257
11304
|
const count = artifactFailures.length;
|
|
@@ -12316,7 +12363,7 @@ Usage:
|
|
|
12316
12363
|
|
|
12317
12364
|
${DESIGN_SYSTEM_HINT}
|
|
12318
12365
|
|
|
12319
|
-
Note: poll long-polls
|
|
12366
|
+
Note: poll long-polls until the user sends feedback, ends the session, or leaves every review window disconnected past the reconnect grace period, staying silent while it waits - never kill it. Layout issues the browser detects are passive: they collect in the user's Layout issues inbox in the Lavish top bar and reach the agent only when the user selects them and queues the fixes, as an ordinary tag "layout-warnings" prompt. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. ${pollExecutionGuidance({ agent })} ${POLL_SEND_AND_END_RULE}
|
|
12320
12367
|
|
|
12321
12368
|
`;
|
|
12322
12369
|
}
|
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",
|