lavish-axi 0.1.80 → 0.1.82
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 +221 -6
- package/dist/chrome.css +47 -0
- package/dist/cli.mjs +30 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -197,18 +197,19 @@ pnpm link
|
|
|
197
197
|
Unresolved local assets or export notices such as author-set CSP meta tags and redacted file URLs are surfaced in command or browser output.
|
|
198
198
|
Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token when publishing a new page (a republish or `--unpublish` authorizes with the `update_key` instead and rejects `--token`); set `LAVISH_AXI_HTML_APP_API_URL` to override the ht-ml.app API base and point `share` at a backend you control (see [Self-hosting the share backend](docs/self-hosting-share.md) for the contract it must implement).
|
|
199
199
|
- **Live reload** - Lavish watches the HTML artifact file by default and preserves review context across reloads: the artifact iframe scroll position, an open annotation card's unsent text, and answers to `data-lavish-question` controls (application-owned form state is left alone). Unsent annotation text also survives a full reload of the review page itself. While a queued layout-issue batch is outstanding, closely spaced saves coalesce so one batch of fixes costs one refresh. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
|
|
200
|
-
If the element an unsent annotation was attached to is gone from the artifact for good, Lavish cannot reopen that card,
|
|
201
|
-
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
200
|
+
If the element an unsent annotation was attached to is gone from the artifact for good, Lavish cannot reopen that card, and if the artifact replaces a queued note while you are editing it, the replacement is kept as the artifact sent it. Either way, Lavish writes your edited text into the conversation panel under **Unsent annotation** - selectable, never written over anything you have typed, and kept there across reloads; no note is ever dropped to make room for a newer one, and a note the browser refuses to store says so where it is shown.
|
|
201
|
+
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) and custom widgets with an interactive ARIA role (`button`, `checkbox`, `combobox`, `menuitem`, `menuitemcheckbox`, `menuitemradio`, `option`, `radio`, `switch`, `tab`, `treeitem`) are interactive automatically, so they do not need `data-lavish-action`. A link (`<a href>`) stays annotatable when it carries one of these roles or sits inside such a widget.
|
|
202
202
|
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()`.
|
|
203
203
|
If someone may open a decision artifact as a standalone HTML file, `lavish-axi playbook input` provides an optional Copy all answers control they can use to paste their form answers back into chat.
|
|
204
204
|
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.
|
|
205
|
-
Everything you send is part of the conversation: a note on an element, a text selection, a table cell, a diagram node, a whiteboard, a layout-issue batch, or a composer message each becomes a bubble on your side of the Conversation panel, with an anchor line naming what it was attached to (the element tag, text selection, table cell, diagram node, whiteboard, or layout issues), a quoted excerpt, and, when available, the selector on hover. A note you have queued but not sent sits at the end of the conversation as a dashed bubble labelled **Queued** with
|
|
205
|
+
Everything you send is part of the conversation: a note on an element, a text selection, a table cell, a diagram node, a whiteboard, a layout-issue batch, or a composer message each becomes a bubble on your side of the Conversation panel, with an anchor line naming what it was attached to (the element tag, text selection, table cell, diagram node, whiteboard, or layout issues), a quoted excerpt, and, when available, the selector on hover. A note you have queued but not sent sits at the end of the conversation as a dashed bubble labelled **Queued** with edit and remove controls. Clicking the bubble or its edit control opens its words for editing in place and scrolls the artifact to the element it points at; clicking that element again in annotate mode opens the same note rather than a fresh card. Saving a note with no words and no images removes it, and **Send** delivers an open edit as typed. While its batch is in flight it reads **Sending** and can no longer be edited, and once the server accepts it, it settles in place as a sent bubble in every tab of the session. The panel keeps the newest stored transcript up to 5 MiB; older bubbles drop from view, but an accepted note still settles and is not sent twice. Agent replies render a small Markdown subset - paragraphs, line breaks, headings as bold lead lines, bullet and numbered lists, fenced and inline code, bold, italic, and http(s) links - while tables, images, and raw HTML stay as the text the agent wrote; your own messages keep their line breaks and are never parsed.
|
|
206
206
|
On wider screens, the conversation and the queued notes share one scrollable Conversation panel above a sticky composer, so long feedback queues do not push the text box or send controls off screen.
|
|
207
207
|
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.
|
|
208
208
|
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 unless a transcript sync proves the server already accepted that feedback.
|
|
209
209
|
- **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.
|
|
210
210
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
211
211
|
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.
|
|
212
|
+
While editing a queued note, Enter saves it, Shift+Enter inserts a newline, and Escape discards the edit.
|
|
212
213
|
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.
|
|
213
214
|
- **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. An agent reply concludes delivered work and returns presence to waiting (see the CLI Reference for reply and poll commands). A supervisor-owned process listener is shown as an external listener rather than an idle captain turn.
|
|
214
215
|
In a Herdr-managed pane, set `LAVISH_AXI_HERDR_CHIME=1` to show a Herdr request notification and play its chime once an indefinite poll enters the waiting state. Immediate queued feedback and timed debug polls do not trigger a notification. The integration is off unless both `LAVISH_AXI_HERDR_CHIME` and Herdr's `HERDR_ENV` session marker equal `1`; leaving either unset or using any other value opts out. Notification delivery runs without blocking feedback: if Herdr is unavailable, rejects the notification, stalls, or fails, the poll continues unchanged.
|
package/dist/chrome-client.js
CHANGED
|
@@ -261,6 +261,12 @@ let terminalSubmission =
|
|
|
261
261
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
262
262
|
let sendAcknowledgementTimer;
|
|
263
263
|
let lastScroll = { x: 0, y: 0 };
|
|
264
|
+
// The queued note open for editing in the panel, by prompt identity, and its unsaved words.
|
|
265
|
+
let editingPromptId = "";
|
|
266
|
+
let editingDraft = "";
|
|
267
|
+
let editFocusPending = false;
|
|
268
|
+
// The selector list last told to the artifact, so a render that changes nothing stays quiet.
|
|
269
|
+
let postedQueuedAnchors = "[]";
|
|
264
270
|
// In-iframe review context (an open annotation card's unsent text, Lavish-owned question
|
|
265
271
|
// answers). The sandbox means the chrome cannot read it back after a reload, so the SDK reports
|
|
266
272
|
// it as it changes and the chrome replays it once the new document is up. It is persisted per
|
|
@@ -486,6 +492,8 @@ function persistTerminalReservation(reserved) {
|
|
|
486
492
|
|
|
487
493
|
const REMOVE_ICON_SVG =
|
|
488
494
|
'<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
|
|
495
|
+
const EDIT_ICON_SVG =
|
|
496
|
+
'<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M6.5 1.5L8.5 3.5L3.5 8.5H1.5V6.5L6.5 1.5Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>';
|
|
489
497
|
const ANCHOR_EXCERPT_MAX = 120;
|
|
490
498
|
const ANCHOR_SELECTOR_MAX = 512;
|
|
491
499
|
const ANCHOR_LABEL_MAX = 40;
|
|
@@ -587,14 +595,23 @@ function userBubbleTextHtml(entry, text) {
|
|
|
587
595
|
}
|
|
588
596
|
|
|
589
597
|
// A queued note is the user bubble in its not-yet-sent state: dashed, labelled Queued (Sending
|
|
590
|
-
// while its batch is in flight), and removable until then. It settles in place as a
|
|
591
|
-
// once the server's transcript carries it, so nothing moves between regions.
|
|
598
|
+
// while its batch is in flight), and editable and removable until then. It settles in place as a
|
|
599
|
+
// sent bubble once the server's transcript carries it, so nothing moves between regions.
|
|
592
600
|
function queuedBubbleHtml(prompt, index) {
|
|
593
601
|
const sending = isPromptSending(prompt);
|
|
602
|
+
if (!sending && editingPromptId && promptIdentity(prompt) === editingPromptId) {
|
|
603
|
+
return queuedEditorHtml(prompt, index);
|
|
604
|
+
}
|
|
594
605
|
return (
|
|
595
|
-
'<div class="bubble user queued"
|
|
606
|
+
'<div class="bubble user queued" data-index="' +
|
|
607
|
+
index +
|
|
608
|
+
'"><small>' +
|
|
596
609
|
(sending ? "Sending\u2026" : "Queued") +
|
|
597
|
-
' <button class="queued-
|
|
610
|
+
' <button class="queued-edit" type="button" aria-label="Edit queued prompt" data-index="' +
|
|
611
|
+
index +
|
|
612
|
+
'">' +
|
|
613
|
+
EDIT_ICON_SVG +
|
|
614
|
+
'</button><button class="queued-remove" type="button" aria-label="Remove queued prompt" data-index="' +
|
|
598
615
|
index +
|
|
599
616
|
'">' +
|
|
600
617
|
REMOVE_ICON_SVG +
|
|
@@ -606,6 +623,25 @@ function queuedBubbleHtml(prompt, index) {
|
|
|
606
623
|
);
|
|
607
624
|
}
|
|
608
625
|
|
|
626
|
+
// The same bubble while its words are being revised. The anchor and images stay as they were:
|
|
627
|
+
// an edit changes what the note says, not what it points at.
|
|
628
|
+
function queuedEditorHtml(prompt, index) {
|
|
629
|
+
return (
|
|
630
|
+
'<div class="bubble user queued editing" data-index="' +
|
|
631
|
+
index +
|
|
632
|
+
'"><small>Editing</small>' +
|
|
633
|
+
anchorHtml(promptAnchor(prompt)) +
|
|
634
|
+
'<textarea class="queued-edit-input" aria-label="Edit queued prompt" data-index="' +
|
|
635
|
+
index +
|
|
636
|
+
'">' +
|
|
637
|
+
escapeHtml(editingDraft) +
|
|
638
|
+
'</textarea><div class="queued-edit-actions"><button class="queued-edit-cancel" type="button">Cancel</button>' +
|
|
639
|
+
'<button class="queued-edit-save" type="button">Save</button></div>' +
|
|
640
|
+
bubbleAttachmentsHtml(prompt) +
|
|
641
|
+
"</div>"
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
609
645
|
// Whether a queued prompt is committed to a send that has not been answered yet: waiting for its
|
|
610
646
|
// snapshot, queued behind another submission, in flight, or held by the terminal reservation.
|
|
611
647
|
// Derived from the existing send bookkeeping rather than tracked separately, so no failure path
|
|
@@ -624,6 +660,23 @@ function isPromptSending(prompt) {
|
|
|
624
660
|
}
|
|
625
661
|
|
|
626
662
|
function render() {
|
|
663
|
+
const editing = editingPromptId ? queued.find((prompt) => promptIdentity(prompt) === editingPromptId) : null;
|
|
664
|
+
// A note that left the queue or started sending takes its editor with it.
|
|
665
|
+
if (editingPromptId && (!editing || !isPromptEditable(editing))) endQueuedEdit();
|
|
666
|
+
const keepEditFocus = editFocusPending || queuedLog.contains(document.activeElement);
|
|
667
|
+
// A re-render while the reviewer is typing keeps their caret and scroll; only a freshly opened
|
|
668
|
+
// edit starts at the end of its words.
|
|
669
|
+
const previousInput = /** @type {HTMLTextAreaElement | null} */ (queuedLog.querySelector(".queued-edit-input"));
|
|
670
|
+
const caret =
|
|
671
|
+
!editFocusPending && previousInput && previousInput === document.activeElement
|
|
672
|
+
? {
|
|
673
|
+
start: previousInput.selectionStart,
|
|
674
|
+
end: previousInput.selectionEnd,
|
|
675
|
+
direction: previousInput.selectionDirection,
|
|
676
|
+
scrollTop: previousInput.scrollTop,
|
|
677
|
+
}
|
|
678
|
+
: null;
|
|
679
|
+
editFocusPending = false;
|
|
627
680
|
queuedLog.innerHTML = queued.map((prompt, index) => queuedBubbleHtml(prompt, index)).join("");
|
|
628
681
|
|
|
629
682
|
for (const button of queuedLog.querySelectorAll(".queued-remove")) {
|
|
@@ -632,6 +685,23 @@ function render() {
|
|
|
632
685
|
removeButton.disabled = terminalSubmission !== null || isPromptSending(prompt);
|
|
633
686
|
removeButton.addEventListener("click", (event) => removeQueuedPrompt(Number(removeButton.dataset.index), event));
|
|
634
687
|
}
|
|
688
|
+
for (const button of queuedLog.querySelectorAll(".queued-edit")) {
|
|
689
|
+
const editButton = /** @type {HTMLButtonElement} */ (button);
|
|
690
|
+
editButton.disabled = !isPromptEditable(queued[Number(editButton.dataset.index)]);
|
|
691
|
+
}
|
|
692
|
+
if (editingPromptId && keepEditFocus) {
|
|
693
|
+
const input = /** @type {HTMLTextAreaElement | null} */ (queuedLog.querySelector(".queued-edit-input"));
|
|
694
|
+
if (input) {
|
|
695
|
+
input.focus();
|
|
696
|
+
if (caret) {
|
|
697
|
+
input.setSelectionRange?.(caret.start, caret.end, caret.direction);
|
|
698
|
+
input.scrollTop = caret.scrollTop;
|
|
699
|
+
} else {
|
|
700
|
+
input.setSelectionRange?.(input.value.length, input.value.length);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
postQueuedAnchors();
|
|
635
705
|
updateSendState();
|
|
636
706
|
scrollPanelToBottom();
|
|
637
707
|
renderSheetSummary();
|
|
@@ -1060,6 +1130,7 @@ function setReviewState(state) {
|
|
|
1060
1130
|
}
|
|
1061
1131
|
|
|
1062
1132
|
function hasUnsentDraft() {
|
|
1133
|
+
if (editingPromptId) return true;
|
|
1063
1134
|
return Boolean(lastReviewState && lastReviewState.card && String(lastReviewState.card.text || "").trim());
|
|
1064
1135
|
}
|
|
1065
1136
|
|
|
@@ -1108,7 +1179,7 @@ function renderRetiredDraft(text, stored = true) {
|
|
|
1108
1179
|
const el = document.createElement("div");
|
|
1109
1180
|
el.className = "bubble note";
|
|
1110
1181
|
el.innerHTML =
|
|
1111
|
-
"<small>Unsent annotation</small><div>The
|
|
1182
|
+
"<small>Unsent annotation</small><div>The artifact removed or replaced what this note was attached to, so Lavish could not keep it open. Your text is kept here:</div>" +
|
|
1112
1183
|
'<div class="note-draft">' +
|
|
1113
1184
|
escapeHtml(text) +
|
|
1114
1185
|
"</div>" +
|
|
@@ -1338,16 +1409,149 @@ function scrollElementIntoView(el) {
|
|
|
1338
1409
|
el.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1339
1410
|
}
|
|
1340
1411
|
|
|
1412
|
+
function isPromptEditable(prompt) {
|
|
1413
|
+
return Boolean(prompt) && !terminalSubmission && !isPromptSending(prompt);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function editQueuedPrompt(index, { reveal = true } = {}) {
|
|
1417
|
+
const prompt = queued[index];
|
|
1418
|
+
if (ended || !isPromptEditable(prompt)) return;
|
|
1419
|
+
const id = promptIdentity(prompt);
|
|
1420
|
+
if (!id || id === editingPromptId) return;
|
|
1421
|
+
// Moving to another note keeps what was typed into the first, the way leaving a field does.
|
|
1422
|
+
commitQueuedEdit();
|
|
1423
|
+
editingPromptId = id;
|
|
1424
|
+
editingDraft = String(prompt.prompt || "");
|
|
1425
|
+
editFocusPending = true;
|
|
1426
|
+
if (isMobileSheet()) setSheetOpen(true);
|
|
1427
|
+
render();
|
|
1428
|
+
if (reveal && prompt.selector) postToFrame({ type: "lavish:revealElement", selector: String(prompt.selector) });
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function endQueuedEdit() {
|
|
1432
|
+
editingPromptId = "";
|
|
1433
|
+
editingDraft = "";
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Writes the open edit into its note without rendering. Returns whether the queue changed. A note
|
|
1437
|
+
// cleared of words and images is removed, as if its remove control had been used.
|
|
1438
|
+
function commitQueuedEdit() {
|
|
1439
|
+
if (!editingPromptId) return false;
|
|
1440
|
+
const index = queued.findIndex((prompt) => promptIdentity(prompt) === editingPromptId);
|
|
1441
|
+
const text = editingDraft.trim();
|
|
1442
|
+
endQueuedEdit();
|
|
1443
|
+
const prompt = queued[index];
|
|
1444
|
+
if (!isPromptEditable(prompt)) return false;
|
|
1445
|
+
if (!text && !attachmentCount(prompt)) {
|
|
1446
|
+
queued.splice(index, 1);
|
|
1447
|
+
afterQueuedPromptRemoved();
|
|
1448
|
+
} else {
|
|
1449
|
+
// In place, not replaced: the send bookkeeping tracks notes by object identity.
|
|
1450
|
+
prompt.prompt = text;
|
|
1451
|
+
persistQueuedPrompts();
|
|
1452
|
+
}
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function saveQueuedEdit() {
|
|
1457
|
+
commitQueuedEdit();
|
|
1458
|
+
render();
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function cancelQueuedEdit() {
|
|
1462
|
+
endQueuedEdit();
|
|
1463
|
+
render();
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// A click on an element the artifact knows carries a queued note. The artifact names only the
|
|
1467
|
+
// selector, so the most a forged message can do is open one of the user's own notes for editing.
|
|
1468
|
+
function editQueuedAnchor(selector) {
|
|
1469
|
+
if (!selector) return;
|
|
1470
|
+
for (let index = queued.length - 1; index >= 0; index--) {
|
|
1471
|
+
if (isElementAnchoredPrompt(queued[index]) && queuedAnchorSelectorsOf(queued[index]).includes(selector)) {
|
|
1472
|
+
if (isPromptEditable(queued[index])) {
|
|
1473
|
+
editQueuedPrompt(index, { reveal: false });
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// The note is already on its way, or gone: the click still deserves the card it would have had.
|
|
1480
|
+
postToFrame({ type: "lavish:annotateElement", selector });
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Notes that point at a whole element, cell, or diagram node, which a later click on that same
|
|
1484
|
+
// target can find again. A text selection's note is about the words, not the element around them.
|
|
1485
|
+
function isElementAnchoredPrompt(prompt) {
|
|
1486
|
+
const kind = promptAnchor(prompt)?.kind;
|
|
1487
|
+
return Boolean(prompt.selector) && (kind === "element" || kind === "cell" || kind === "node");
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// A diagram node is one target however deep the click lands, so its note answers to the node's
|
|
1491
|
+
// own selector as well as the clicked element's. Other notes name only the exact element.
|
|
1492
|
+
function queuedAnchorSelectorsOf(prompt) {
|
|
1493
|
+
const target = prompt.target && typeof prompt.target === "object" ? prompt.target : null;
|
|
1494
|
+
const whole = target?.type === "mermaid-node" ? String(target.selector || "") : "";
|
|
1495
|
+
return whole ? [String(prompt.selector), whole] : [String(prompt.selector)];
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Tells the artifact which element selectors carry a queued note. Only selectors cross into the
|
|
1499
|
+
// frame: the artifact is agent-authored, and the note text stays in the chrome.
|
|
1500
|
+
function postQueuedAnchors(force = false) {
|
|
1501
|
+
const selectors = [
|
|
1502
|
+
...new Set(queued.filter((prompt) => isElementAnchoredPrompt(prompt)).flatMap(queuedAnchorSelectorsOf)),
|
|
1503
|
+
];
|
|
1504
|
+
const signature = JSON.stringify(selectors);
|
|
1505
|
+
if (!force && signature === postedQueuedAnchors) return;
|
|
1506
|
+
postedQueuedAnchors = signature;
|
|
1507
|
+
postToFrame({ type: "lavish:queuedAnchors", selectors });
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
queuedLog.addEventListener("click", (event) => {
|
|
1511
|
+
const target = /** @type {Element | null} */ (event.target);
|
|
1512
|
+
if (!target || typeof target.closest !== "function") return;
|
|
1513
|
+
// The remove control owns its own click, and an editor's textarea is for typing.
|
|
1514
|
+
if (target.closest(".queued-remove") || target.closest(".queued-edit-input")) return;
|
|
1515
|
+
if (target.closest(".queued-edit-save")) return saveQueuedEdit();
|
|
1516
|
+
if (target.closest(".queued-edit-cancel")) return cancelQueuedEdit();
|
|
1517
|
+
const bubble = /** @type {HTMLElement | null} */ (target.closest(".queued-edit") || target.closest(".bubble.queued"));
|
|
1518
|
+
if (bubble) editQueuedPrompt(Number(bubble.dataset.index));
|
|
1519
|
+
});
|
|
1520
|
+
queuedLog.addEventListener("input", (event) => {
|
|
1521
|
+
const target = /** @type {HTMLTextAreaElement | null} */ (event.target);
|
|
1522
|
+
if (target && typeof target.closest === "function" && target.closest(".queued-edit-input")) {
|
|
1523
|
+
editingDraft = String(target.value || "");
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
queuedLog.addEventListener("keydown", (event) => {
|
|
1527
|
+
const target = /** @type {Element | null} */ (event.target);
|
|
1528
|
+
if (!target || typeof target.closest !== "function" || !target.closest(".queued-edit-input")) return;
|
|
1529
|
+
// The annotation card's keys: Enter keeps the words, Shift+Enter breaks the line.
|
|
1530
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
1531
|
+
event.preventDefault();
|
|
1532
|
+
event.stopPropagation();
|
|
1533
|
+
saveQueuedEdit();
|
|
1534
|
+
} else if (event.key === "Escape" && !event.isComposing) {
|
|
1535
|
+
event.preventDefault();
|
|
1536
|
+
event.stopPropagation();
|
|
1537
|
+
cancelQueuedEdit();
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1341
1541
|
function removeQueuedPrompt(index, event) {
|
|
1342
1542
|
if (event) event.stopPropagation();
|
|
1343
1543
|
if (terminalSubmission || isPromptSending(queued[index])) return;
|
|
1344
1544
|
queued.splice(index, 1);
|
|
1545
|
+
afterQueuedPromptRemoved();
|
|
1546
|
+
render();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function afterQueuedPromptRemoved() {
|
|
1345
1550
|
persistQueuedPrompts();
|
|
1346
1551
|
if (!queued.length) {
|
|
1347
1552
|
clearSendAcknowledgementWarning();
|
|
1348
1553
|
if (sendFailureOwner?.kind !== "preparation") clearPersistentSendFailure();
|
|
1349
1554
|
}
|
|
1350
|
-
render();
|
|
1351
1555
|
}
|
|
1352
1556
|
|
|
1353
1557
|
function promptQueueKey(prompt) {
|
|
@@ -1382,6 +1586,13 @@ function enqueuePrompt(rawPrompt, /** @type {FeedbackPreparation | null} */ prep
|
|
|
1382
1586
|
if (queueKey) {
|
|
1383
1587
|
const index = queued.findIndex((item) => promptQueueKey(item) === queueKey);
|
|
1384
1588
|
if (index !== -1) {
|
|
1589
|
+
// The replacement is the artifact's newer answer. Words typed against the old answer never
|
|
1590
|
+
// move onto it; they are handed back to the reviewer instead.
|
|
1591
|
+
if (editingPromptId && promptIdentity(queued[index]) === editingPromptId) {
|
|
1592
|
+
const draft = editingDraft.trim();
|
|
1593
|
+
endQueuedEdit();
|
|
1594
|
+
if (draft !== String(queued[index].prompt || "").trim()) keepRetiredDraft(draft);
|
|
1595
|
+
}
|
|
1385
1596
|
queued[index] = prompt;
|
|
1386
1597
|
} else {
|
|
1387
1598
|
queued.push(prompt);
|
|
@@ -1660,6 +1871,8 @@ function sendQueued(endAfter) {
|
|
|
1660
1871
|
return;
|
|
1661
1872
|
}
|
|
1662
1873
|
closeMenus();
|
|
1874
|
+
// Send delivers what the panel shows, so an open edit is kept rather than sent stale.
|
|
1875
|
+
if (commitQueuedEdit()) render();
|
|
1663
1876
|
|
|
1664
1877
|
// A pending or failed chip holds back only the COMPOSER message (and an
|
|
1665
1878
|
// explicit end, which would strand the chips) - queued annotation prompts
|
|
@@ -3965,6 +4178,7 @@ window.addEventListener("message", (event) => {
|
|
|
3965
4178
|
if (msg.type === "lavish:endSession") endSession();
|
|
3966
4179
|
if (msg.type === "lavish:revisions") applyRevisionMessage(msg);
|
|
3967
4180
|
if (msg.type === "lavish:toggleAnnotationMode") toggleAnnotationMode();
|
|
4181
|
+
if (msg.type === "lavish:editQueuedAnchor") editQueuedAnchor(String(msg.selector || ""));
|
|
3968
4182
|
});
|
|
3969
4183
|
|
|
3970
4184
|
// The sandboxed artifact iframe can't reach the loopback server (opaque origin),
|
|
@@ -4293,6 +4507,7 @@ frame.addEventListener("load", () => {
|
|
|
4293
4507
|
// Replay the pre-reload scroll position so hot reloads don't jump the artifact to the top.
|
|
4294
4508
|
postToFrame({ type: "lavish:restoreScroll", x: lastScroll.x, y: lastScroll.y });
|
|
4295
4509
|
if (lastReviewState) postToFrame({ type: "lavish:restoreReviewState", state: lastReviewState });
|
|
4510
|
+
postQueuedAnchors(true);
|
|
4296
4511
|
if (overlayIndex !== null) {
|
|
4297
4512
|
inlineWhiteboardChannels.delete(overlayIndex);
|
|
4298
4513
|
postToFrame({ type: "lavish:suspendWhiteboard", diagramIndex: overlayIndex });
|
package/dist/chrome.css
CHANGED
|
@@ -1199,6 +1199,15 @@ body.lavish {
|
|
|
1199
1199
|
align-items: center;
|
|
1200
1200
|
gap: 8px;
|
|
1201
1201
|
}
|
|
1202
|
+
.bubble.queued:not(.editing) {
|
|
1203
|
+
cursor: pointer;
|
|
1204
|
+
}
|
|
1205
|
+
.bubble.queued.editing {
|
|
1206
|
+
align-self: stretch;
|
|
1207
|
+
max-width: 100%;
|
|
1208
|
+
border-color: var(--accent);
|
|
1209
|
+
}
|
|
1210
|
+
.queued-edit,
|
|
1202
1211
|
.queued-remove {
|
|
1203
1212
|
display: flex;
|
|
1204
1213
|
align-items: center;
|
|
@@ -1212,13 +1221,51 @@ body.lavish {
|
|
|
1212
1221
|
color: var(--fg-muted);
|
|
1213
1222
|
cursor: pointer;
|
|
1214
1223
|
}
|
|
1224
|
+
.queued-edit:disabled,
|
|
1215
1225
|
.queued-remove:disabled {
|
|
1216
1226
|
opacity: 0.55;
|
|
1217
1227
|
cursor: not-allowed;
|
|
1218
1228
|
}
|
|
1229
|
+
.queued-edit svg,
|
|
1219
1230
|
.queued-remove svg {
|
|
1220
1231
|
display: block;
|
|
1221
1232
|
}
|
|
1233
|
+
.queued-edit-input {
|
|
1234
|
+
display: block;
|
|
1235
|
+
width: 100%;
|
|
1236
|
+
min-height: 64px;
|
|
1237
|
+
resize: vertical;
|
|
1238
|
+
border-radius: var(--radius-md);
|
|
1239
|
+
border: 1px solid var(--border);
|
|
1240
|
+
background: var(--bg);
|
|
1241
|
+
color: var(--fg);
|
|
1242
|
+
padding: 8px;
|
|
1243
|
+
font: inherit;
|
|
1244
|
+
box-sizing: border-box;
|
|
1245
|
+
}
|
|
1246
|
+
.queued-edit-actions {
|
|
1247
|
+
display: flex;
|
|
1248
|
+
justify-content: flex-end;
|
|
1249
|
+
gap: 8px;
|
|
1250
|
+
margin-top: 8px;
|
|
1251
|
+
}
|
|
1252
|
+
.queued-edit-actions button {
|
|
1253
|
+
border: 0;
|
|
1254
|
+
border-radius: var(--radius-md);
|
|
1255
|
+
padding: 6px 10px;
|
|
1256
|
+
font-family: inherit;
|
|
1257
|
+
font-size: 12px;
|
|
1258
|
+
font-weight: var(--w-bold);
|
|
1259
|
+
cursor: pointer;
|
|
1260
|
+
}
|
|
1261
|
+
.queued-edit-cancel {
|
|
1262
|
+
background: var(--border);
|
|
1263
|
+
color: var(--fg);
|
|
1264
|
+
}
|
|
1265
|
+
.queued-edit-save {
|
|
1266
|
+
background: var(--accent);
|
|
1267
|
+
color: var(--accent-ink);
|
|
1268
|
+
}
|
|
1222
1269
|
/* The anchor line names what a note is attached to, in the annotation card's words. */
|
|
1223
1270
|
.anchor {
|
|
1224
1271
|
display: flex;
|
package/dist/cli.mjs
CHANGED
|
@@ -224,7 +224,7 @@ var PLAYBOOKS = [
|
|
|
224
224
|
"Show selected state separately from queued state so the user trusts what will be sent back."
|
|
225
225
|
],
|
|
226
226
|
design_rules: [
|
|
227
|
-
"Native controls - radios, checkboxes, text inputs, selects, textareas, buttons, options, labels, disclosure summaries, and contenteditable regions - are interactive automatically: clicks toggle, focus, and type instead of annotating, so they do not need data-lavish-action. Build choice and option UIs from these whenever you can.",
|
|
227
|
+
"Native controls - radios, checkboxes, text inputs, selects, textareas, buttons, options, labels, disclosure summaries, and contenteditable regions - and custom widgets with an interactive ARIA role (button, checkbox, combobox, menuitem, option, radio, switch, tab, treeitem, and the menuitem variants) are interactive automatically: clicks toggle, focus, and type instead of annotating, so they do not need data-lavish-action. An <a href> stays annotatable when it carries one of these roles or sits inside such a widget. Build choice and option UIs from these whenever you can.",
|
|
228
228
|
"For reversible choices, do not call window.lavish.queuePrompt() from radio change handlers or option click handlers. Those handlers should only update local selected state.",
|
|
229
229
|
"Use a per-question form submit or explicit Queue answer button to read the current values and call window.lavish.queuePrompt() exactly once for the final answer.",
|
|
230
230
|
"Put data-lavish-action only on custom (non-native) elements that should act like a feedback control - typically a styled div or span you made clickable - so Lavish does not annotate it and shows a pointer cursor instead.",
|
|
@@ -5284,9 +5284,18 @@ function deriveLavishQueueKey(element, options = {}) {
|
|
|
5284
5284
|
return "";
|
|
5285
5285
|
}
|
|
5286
5286
|
function isNativeInteractiveControl(el) {
|
|
5287
|
-
|
|
5287
|
+
if (!el || !el.closest) return false;
|
|
5288
|
+
if (el.closest(
|
|
5288
5289
|
"button,input,select,textarea,option,optgroup,label,summary,[contenteditable]:not([contenteditable='false'])"
|
|
5289
|
-
))
|
|
5290
|
+
)) {
|
|
5291
|
+
return true;
|
|
5292
|
+
}
|
|
5293
|
+
const widget = el.closest(
|
|
5294
|
+
"[role='button'],[role='checkbox'],[role='combobox'],[role='menuitem'],[role='menuitemcheckbox'],[role='menuitemradio'],[role='option'],[role='radio'],[role='switch'],[role='tab'],[role='treeitem']"
|
|
5295
|
+
);
|
|
5296
|
+
if (!widget) return false;
|
|
5297
|
+
const link = el.closest("a[href]");
|
|
5298
|
+
return !(link && widget.contains(link));
|
|
5290
5299
|
}
|
|
5291
5300
|
function classifySevereTextOverflow({
|
|
5292
5301
|
fragments,
|
|
@@ -5487,6 +5496,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
5487
5496
|
let hovered = null;
|
|
5488
5497
|
let selected = null;
|
|
5489
5498
|
let ignoreNextClick = false;
|
|
5499
|
+
let queuedAnchorSelectors = /* @__PURE__ */ new Set();
|
|
5490
5500
|
let shadow = null;
|
|
5491
5501
|
let counter = 0;
|
|
5492
5502
|
const ids = /* @__PURE__ */ new WeakMap();
|
|
@@ -6000,7 +6010,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
6000
6010
|
if (annotationMode && !style) {
|
|
6001
6011
|
style = document.createElement("style");
|
|
6002
6012
|
style.id = "lavish-cursor-style";
|
|
6003
|
-
style.textContent = ":root{--lavish-accent:#f4c95d;--lavish-annotate-outline:2px solid var(--lavish-accent);--lavish-annotate-offset:2px}*{cursor:default!important}[data-lavish-action],[data-lavish-action] *{cursor:pointer!important}input,textarea,[contenteditable]:not([contenteditable='false']){cursor:text!important}button,select,label,option,input[type='button'],input[type='submit'],input[type='reset'],input[type='checkbox'],input[type='radio'],input[type='file'],input[type='color'],input[type='range'],input[type='image']{cursor:pointer!important}";
|
|
6013
|
+
style.textContent = ":root{--lavish-accent:#f4c95d;--lavish-annotate-outline:2px solid var(--lavish-accent);--lavish-annotate-offset:2px}*{cursor:default!important}:where(:is([role='button'],[role='checkbox'],[role='combobox'],[role='menuitem'],[role='menuitemcheckbox'],[role='menuitemradio'],[role='option'],[role='radio'],[role='switch'],[role='tab'],[role='treeitem']):not(a[href])),:where(:is([role='button'],[role='checkbox'],[role='combobox'],[role='menuitem'],[role='menuitemcheckbox'],[role='menuitemradio'],[role='option'],[role='radio'],[role='switch'],[role='tab'],[role='treeitem']):not(a[href])) *{cursor:pointer!important}:where(:is([role='button'],[role='checkbox'],[role='combobox'],[role='menuitem'],[role='menuitemcheckbox'],[role='menuitemradio'],[role='option'],[role='radio'],[role='switch'],[role='tab'],[role='treeitem']) a[href]),:where(:is([role='button'],[role='checkbox'],[role='combobox'],[role='menuitem'],[role='menuitemcheckbox'],[role='menuitemradio'],[role='option'],[role='radio'],[role='switch'],[role='tab'],[role='treeitem']) a[href]) *{cursor:default!important}[data-lavish-action],[data-lavish-action] *{cursor:pointer!important}input,textarea,[contenteditable]:not([contenteditable='false']){cursor:text!important}button,select,label,option,input[type='button'],input[type='submit'],input[type='reset'],input[type='checkbox'],input[type='radio'],input[type='file'],input[type='color'],input[type='range'],input[type='image']{cursor:pointer!important}";
|
|
6004
6014
|
document.head.appendChild(style);
|
|
6005
6015
|
}
|
|
6006
6016
|
if (!annotationMode && style) style.remove();
|
|
@@ -7051,6 +7061,13 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
7051
7061
|
}
|
|
7052
7062
|
if (msg.type === "lavish:restoreReviewState") restoreReviewState(msg.state);
|
|
7053
7063
|
if (msg.type === "lavish:revealElement") revealElement(msg.selector);
|
|
7064
|
+
if (msg.type === "lavish:queuedAnchors") {
|
|
7065
|
+
queuedAnchorSelectors = new Set(Array.isArray(msg.selectors) ? msg.selectors.map(String) : []);
|
|
7066
|
+
}
|
|
7067
|
+
if (msg.type === "lavish:annotateElement" && annotationMode) {
|
|
7068
|
+
const target = safeQuerySelector(msg.selector);
|
|
7069
|
+
if (target) showAnnotationCard(target);
|
|
7070
|
+
}
|
|
7054
7071
|
});
|
|
7055
7072
|
function revealElement(selector2) {
|
|
7056
7073
|
const target = selector2 === "html" ? document.documentElement : safeQuerySelector(selector2);
|
|
@@ -7135,6 +7152,14 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
7135
7152
|
ignoreNextClick = false;
|
|
7136
7153
|
return;
|
|
7137
7154
|
}
|
|
7155
|
+
const clicked = queuedAnchorSelectors.size ? context(event.target) : null;
|
|
7156
|
+
const node = clicked?.target?.type === "mermaid-node" ? clicked.target.selector : "";
|
|
7157
|
+
const selector2 = [clicked?.selector, node].find((candidate) => candidate && queuedAnchorSelectors.has(candidate));
|
|
7158
|
+
if (selector2) {
|
|
7159
|
+
closeCard();
|
|
7160
|
+
postArtifactMessage("lavish:editQueuedAnchor", { selector: selector2 });
|
|
7161
|
+
return;
|
|
7162
|
+
}
|
|
7138
7163
|
showAnnotationCard(event.target);
|
|
7139
7164
|
},
|
|
7140
7165
|
true
|
|
@@ -12315,7 +12340,7 @@ var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_J
|
|
|
12315
12340
|
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
12316
12341
|
var POLL_STATE_HEADER = "lavish-poll-state";
|
|
12317
12342
|
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.";
|
|
12318
|
-
var VERSION = "0.1.
|
|
12343
|
+
var VERSION = "0.1.82";
|
|
12319
12344
|
function detectInvokingAgent(env = process.env) {
|
|
12320
12345
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
12321
12346
|
}
|
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.82",
|
|
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",
|