lavish-axi 0.1.69 → 0.1.71
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 +20 -19
- package/dist/chrome-client.js +64 -15
- package/dist/cli.mjs +221 -24
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -197,7 +197,7 @@ pnpm link
|
|
|
197
197
|
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
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
|
-
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 a remove control; while its batch is in flight it reads **Sending**, and once the server accepts it, it settles in place as a sent bubble
|
|
200
|
+
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 a remove control; while its batch is in flight it reads **Sending**, 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.
|
|
201
201
|
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.
|
|
202
202
|
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.
|
|
203
203
|
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.
|
|
@@ -205,7 +205,7 @@ pnpm link
|
|
|
205
205
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
206
206
|
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.
|
|
207
207
|
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.
|
|
208
|
-
- **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.
|
|
208
|
+
- **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.
|
|
209
209
|
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.
|
|
210
210
|
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.
|
|
211
211
|
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.
|
|
@@ -272,23 +272,24 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
272
272
|
|
|
273
273
|
### Flags
|
|
274
274
|
|
|
275
|
-
| Command | Flag
|
|
276
|
-
| ------------------------ |
|
|
277
|
-
| `lavish-axi <html-file>` | `--no-open`
|
|
278
|
-
| `lavish-axi <html-file>` | `--no-gate`
|
|
279
|
-
| `lavish-axi <html-file>` | `--reopen`
|
|
280
|
-
| `lavish-axi update` | `--check`
|
|
281
|
-
| `lavish-axi export` | `--out <path>`
|
|
282
|
-
| `lavish-axi share` | `--private`
|
|
283
|
-
| `lavish-axi share` | `--password <pw>`
|
|
284
|
-
| `lavish-axi share` | `--site <site_id>`
|
|
285
|
-
| `lavish-axi share` | `--update-key <key>`
|
|
286
|
-
| `lavish-axi share` | `--unpublish`
|
|
287
|
-
| `lavish-axi share` | `--token <t>`
|
|
288
|
-
| `lavish-axi poll` | `--agent-reply "..."`
|
|
289
|
-
| `lavish-axi poll` | `--
|
|
290
|
-
| `lavish-axi
|
|
291
|
-
| `lavish-axi
|
|
275
|
+
| Command | Flag | Description |
|
|
276
|
+
| ------------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
278
|
+
| `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
|
|
279
|
+
| `lavish-axi <html-file>` | `--reopen` | Reopen a session the user explicitly ended from the browser; without it, a plain open refuses and explains why instead of reopening uninvited. |
|
|
280
|
+
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
281
|
+
| `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
|
|
282
|
+
| `lavish-axi share` | `--private` | Make the page private behind a password Lavish generates and returns once; hand it to viewers as a shared secret. |
|
|
283
|
+
| `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private with a password you supply; viewers must supply the password. An empty or whitespace-only value (an unquoted `$PW` that is unset) is refused rather than publishing a public page. |
|
|
284
|
+
| `lavish-axi share` | `--site <site_id>` | Republish an existing page in place (with `--update-key`) instead of creating a new one; the URL does not change. |
|
|
285
|
+
| `lavish-axi share` | `--update-key <key>` | The secret returned when the page was published; required to republish or unpublish it. |
|
|
286
|
+
| `lavish-axi share` | `--unpublish` | Replace a published page with a locked placeholder (ht-ml.app cannot delete); takes `--site` and `--update-key` and no file. |
|
|
287
|
+
| `lavish-axi share` | `--token <t>` | Attach an optional bearer token (`LAVISH_AXI_HTML_APP_TOKEN`) when creating a page; never required, and rejected on a republish or `--unpublish`, where the `update_key` is the credential. |
|
|
288
|
+
| `lavish-axi poll` | `--agent-reply "..."` | Show a concise agent reply in the existing browser chat, conclude delivered work, and return presence to waiting before polling again. Keep replies concise by default. |
|
|
289
|
+
| `lavish-axi poll` | `--agent-reply-file <path>` | When a longer reply is genuinely necessary, read Markdown from a file (`-` for stdin) so newlines survive quoting. Use blank-line paragraphs, `- ` / `1. ` lists, and `## ` headings so the Conversation panel renders it scannably. The Markdown subset is under Feedback controls. Cannot be combined with `--agent-reply`. |
|
|
290
|
+
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
|
291
|
+
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
|
292
|
+
| `lavish-axi server` | `--verbose` | Log session and watcher events to stderr; can also be enabled with `LAVISH_AXI_DEBUG=1`. Detached server output is appended to `~/.lavish-axi/server.log` (or `LAVISH_AXI_STATE_DIR/server.log`) for startup and crash diagnostics. |
|
|
292
293
|
|
|
293
294
|
## Development
|
|
294
295
|
|
package/dist/chrome-client.js
CHANGED
|
@@ -23,6 +23,8 @@ const promptIdentityField = "prompt_id";
|
|
|
23
23
|
const PROMPT_IDENTITY_MAX = 128;
|
|
24
24
|
const PROMPT_IDENTITY_RE = /^[A-Za-z0-9_-]+$/;
|
|
25
25
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
26
|
+
const initialChatAckIds = Array.isArray(sessionData.initialChatAckIds) ? sessionData.initialChatAckIds : [];
|
|
27
|
+
const initialChatRevision = parseChatRevision(sessionData.initialChatRevision) || 0;
|
|
26
28
|
const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
|
|
27
29
|
const attachmentMaxBytes = Number(sessionData.attachmentMaxBytes) || 0;
|
|
28
30
|
const attachmentMaxCount = Number(sessionData.attachmentMaxCount) || 4;
|
|
@@ -222,6 +224,7 @@ let nextSnapshotRequestId = 0;
|
|
|
222
224
|
let nextSendOperationOrder = 0;
|
|
223
225
|
let workingBubble = null;
|
|
224
226
|
let displayedChat = initialChat.slice();
|
|
227
|
+
let chatRevision = initialChatRevision;
|
|
225
228
|
// Settlement is by per-submission identity, not displayed content: two tabs can queue notes
|
|
226
229
|
// whose chat projection is identical (same selected text under one container, different range
|
|
227
230
|
// boundaries) without settling each other, and a reload after a lost POST response still
|
|
@@ -393,6 +396,15 @@ function promptIdentity(value) {
|
|
|
393
396
|
return isPromptIdentity(value?.[promptIdentityField]) ? value[promptIdentityField] : "";
|
|
394
397
|
}
|
|
395
398
|
|
|
399
|
+
const chatAckIds = new Set();
|
|
400
|
+
function rememberChatAckIds(ids) {
|
|
401
|
+
if (!Array.isArray(ids)) return;
|
|
402
|
+
for (const value of ids) {
|
|
403
|
+
if (isPromptIdentity(value)) chatAckIds.add(value);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
rememberChatAckIds(initialChatAckIds);
|
|
407
|
+
|
|
396
408
|
function createPromptIdentity() {
|
|
397
409
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
398
410
|
return "p" + Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
|
|
@@ -849,13 +861,26 @@ function chatEntriesMatch(left, right) {
|
|
|
849
861
|
return !leftAt || !rightAt || leftAt === rightAt;
|
|
850
862
|
}
|
|
851
863
|
|
|
864
|
+
function parseChatRevision(value) {
|
|
865
|
+
const revision = Number(value);
|
|
866
|
+
return Number.isSafeInteger(revision) && revision >= 0 ? revision : null;
|
|
867
|
+
}
|
|
868
|
+
|
|
852
869
|
function chatContainsEntries(candidate, entries) {
|
|
853
|
-
if (candidate
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
870
|
+
if (!Array.isArray(candidate) || !Array.isArray(entries)) return false;
|
|
871
|
+
if (entries.length === 0) return true;
|
|
872
|
+
// A size-bound transcript may drop a prefix of what this tab already showed. The remaining
|
|
873
|
+
// displayed suffix must still appear in order; a stale sync that is missing a newer tail
|
|
874
|
+
// (including an empty wipe) is rejected.
|
|
875
|
+
for (let start = 0; start < entries.length; start += 1) {
|
|
876
|
+
const suffix = entries.slice(start);
|
|
877
|
+
let matched = 0;
|
|
878
|
+
for (const entry of candidate) {
|
|
879
|
+
if (matched < suffix.length && chatEntriesMatch(entry, suffix[matched])) matched += 1;
|
|
880
|
+
}
|
|
881
|
+
if (matched === suffix.length) return true;
|
|
857
882
|
}
|
|
858
|
-
return
|
|
883
|
+
return false;
|
|
859
884
|
}
|
|
860
885
|
|
|
861
886
|
function chatStartsWith(candidate, prefix) {
|
|
@@ -868,10 +893,18 @@ function chatStartsWith(candidate, prefix) {
|
|
|
868
893
|
}
|
|
869
894
|
|
|
870
895
|
function mergeAcceptedChat(accepted, chatAtRequest) {
|
|
871
|
-
if (!
|
|
896
|
+
if (!Array.isArray(accepted) || !Array.isArray(chatAtRequest)) return null;
|
|
897
|
+
if (!chatStartsWith(displayedChat, chatAtRequest)) return null;
|
|
898
|
+
let requestOffset = chatAtRequest.length;
|
|
899
|
+
for (let i = 0; i <= chatAtRequest.length; i += 1) {
|
|
900
|
+
if (chatStartsWith(accepted, chatAtRequest.slice(i))) {
|
|
901
|
+
requestOffset = i;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
872
905
|
const displayedTail = displayedChat.slice(chatAtRequest.length);
|
|
873
906
|
const unmatchedDisplayed = [];
|
|
874
|
-
let acceptedIndex = chatAtRequest.length;
|
|
907
|
+
let acceptedIndex = chatAtRequest.length - requestOffset;
|
|
875
908
|
for (const displayedEntry of displayedTail) {
|
|
876
909
|
while (acceptedIndex < accepted.length && !chatEntriesMatch(accepted[acceptedIndex], displayedEntry)) {
|
|
877
910
|
acceptedIndex += 1;
|
|
@@ -892,7 +925,9 @@ function queuedPromptMatchesEntry(prompt, entry) {
|
|
|
892
925
|
|
|
893
926
|
function promptAcknowledgedInChat(prompt, chat) {
|
|
894
927
|
const id = promptIdentity(prompt);
|
|
895
|
-
if (!id
|
|
928
|
+
if (!id) return false;
|
|
929
|
+
if (chatAckIds.has(id)) return true;
|
|
930
|
+
if (!Array.isArray(chat)) return false;
|
|
896
931
|
return chat.some((entry) => queuedPromptMatchesEntry(prompt, entry));
|
|
897
932
|
}
|
|
898
933
|
|
|
@@ -916,10 +951,15 @@ function settleQueuedFromTranscript(chat, shouldRender = true) {
|
|
|
916
951
|
return true;
|
|
917
952
|
}
|
|
918
953
|
|
|
919
|
-
function syncChat(chat) {
|
|
954
|
+
function syncChat(chat, revision) {
|
|
920
955
|
const nextChat = Array.isArray(chat) ? chat : [];
|
|
921
956
|
settleQueuedFromTranscript(nextChat);
|
|
922
|
-
|
|
957
|
+
const nextRevision = parseChatRevision(revision);
|
|
958
|
+
if (nextRevision !== null && nextRevision < chatRevision) return false;
|
|
959
|
+
if (nextRevision === null || nextRevision === chatRevision) {
|
|
960
|
+
if (!chatContainsEntries(nextChat, displayedChat)) return false;
|
|
961
|
+
}
|
|
962
|
+
if (nextRevision !== null) chatRevision = nextRevision;
|
|
923
963
|
displayedChat = nextChat.slice();
|
|
924
964
|
for (const el of [...chatLog.querySelectorAll(".bubble.user,.bubble.agent:not(.agent-working)")]) {
|
|
925
965
|
el.remove();
|
|
@@ -1850,17 +1890,23 @@ async function submitQueuedOnce(submission, preserveFailureState = false) {
|
|
|
1850
1890
|
throw new Error("failed to submit queued prompts");
|
|
1851
1891
|
}
|
|
1852
1892
|
const accepted = typeof response.json === "function" ? await response.json().catch(() => null) : null;
|
|
1853
|
-
|
|
1854
|
-
|
|
1893
|
+
rememberChatAckIds(accepted?.ack_ids);
|
|
1894
|
+
const acceptedChat = Array.isArray(accepted?.chat) ? accepted.chat : null;
|
|
1895
|
+
if (acceptedChat) settleQueuedFromTranscript(acceptedChat);
|
|
1896
|
+
const acceptedRevision = parseChatRevision(accepted?.chat_revision);
|
|
1897
|
+
const reconciledChat = acceptedChat
|
|
1898
|
+
? acceptedRevision !== null && acceptedRevision > chatRevision
|
|
1899
|
+
? acceptedChat
|
|
1900
|
+
: mergeAcceptedChat(acceptedChat, submission.chatAtRequest)
|
|
1855
1901
|
: null;
|
|
1856
|
-
if (!
|
|
1902
|
+
if (!acceptedChat || reconciledChat) {
|
|
1857
1903
|
for (const prompt of prompts) {
|
|
1858
1904
|
deliveredPrompts.add(prompt);
|
|
1859
1905
|
const index = queued.indexOf(prompt);
|
|
1860
1906
|
if (index !== -1) queued.splice(index, 1);
|
|
1861
1907
|
}
|
|
1862
1908
|
persistQueuedPrompts();
|
|
1863
|
-
if (reconciledChat) syncChat(reconciledChat);
|
|
1909
|
+
if (reconciledChat) syncChat(reconciledChat, acceptedRevision);
|
|
1864
1910
|
}
|
|
1865
1911
|
render();
|
|
1866
1912
|
settleAcknowledgementGuidance(submission, preserveFailureState);
|
|
@@ -3987,7 +4033,10 @@ events.set("agent-reply", (data) => {
|
|
|
3987
4033
|
if (addChat(entry)) displayedChat.push(entry);
|
|
3988
4034
|
noteAgentReply(entry.text);
|
|
3989
4035
|
});
|
|
3990
|
-
events.set("chat-sync", (data) =>
|
|
4036
|
+
events.set("chat-sync", (data) => {
|
|
4037
|
+
rememberChatAckIds(data.ack_ids);
|
|
4038
|
+
syncChat(data.chat || [], data.chat_revision);
|
|
4039
|
+
});
|
|
3991
4040
|
events.set("agent-presence", (data) => setAgentPresence(data.state));
|
|
3992
4041
|
events.set("layout-warnings", (data) => setLayoutWarnings(data.warnings || []));
|
|
3993
4042
|
events.set("ended", () => markSessionEnded());
|
package/dist/cli.mjs
CHANGED
|
@@ -7,7 +7,16 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// src/cli.js
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
closeSync,
|
|
12
|
+
createReadStream,
|
|
13
|
+
existsSync as existsSync3,
|
|
14
|
+
mkdirSync as mkdirSync2,
|
|
15
|
+
openSync,
|
|
16
|
+
readFileSync as readFileSync2,
|
|
17
|
+
realpathSync,
|
|
18
|
+
writeFileSync as writeFileSync2
|
|
19
|
+
} from "node:fs";
|
|
11
20
|
import { access, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
12
21
|
import os3 from "node:os";
|
|
13
22
|
import path8 from "node:path";
|
|
@@ -7436,6 +7445,7 @@ var SELECTOR_MAX = 512;
|
|
|
7436
7445
|
var LABEL_MAX = 40;
|
|
7437
7446
|
var PROMPT_ID_MAX = 128;
|
|
7438
7447
|
var PROMPT_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
7448
|
+
var MAX_CHAT_STORED_BYTES = 5242880;
|
|
7439
7449
|
function normalizePromptId(value) {
|
|
7440
7450
|
if (typeof value !== "string") return "";
|
|
7441
7451
|
const id = value.trim();
|
|
@@ -7839,6 +7849,52 @@ function serializeChat(chat) {
|
|
|
7839
7849
|
}
|
|
7840
7850
|
return serialized;
|
|
7841
7851
|
}
|
|
7852
|
+
function boundStoredChat(chat, maxBytes = MAX_CHAT_STORED_BYTES) {
|
|
7853
|
+
const entries = Array.isArray(chat) ? chat.filter((entry) => entry && typeof entry === "object") : [];
|
|
7854
|
+
if (entries.length === 0) return { chat: [], evicted: [] };
|
|
7855
|
+
const limit = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : MAX_CHAT_STORED_BYTES;
|
|
7856
|
+
let storedBytes = 2;
|
|
7857
|
+
let cut = entries.length;
|
|
7858
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
7859
|
+
const serialized = JSON.stringify(entries[index]);
|
|
7860
|
+
const entryBytes = Buffer.byteLength(serialized === void 0 ? "null" : serialized, "utf8");
|
|
7861
|
+
const nextBytes = storedBytes + entryBytes + (cut < entries.length ? 1 : 0);
|
|
7862
|
+
if (nextBytes > limit) break;
|
|
7863
|
+
storedBytes = nextBytes;
|
|
7864
|
+
cut = index;
|
|
7865
|
+
}
|
|
7866
|
+
return { chat: entries.slice(cut), evicted: entries.slice(0, cut) };
|
|
7867
|
+
}
|
|
7868
|
+
function serializeChatAckIds(ids) {
|
|
7869
|
+
const ackIds = [];
|
|
7870
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7871
|
+
for (const value of Array.isArray(ids) ? ids : []) {
|
|
7872
|
+
const id = normalizePromptId(value);
|
|
7873
|
+
if (!id || seen.has(id)) continue;
|
|
7874
|
+
seen.add(id);
|
|
7875
|
+
ackIds.push(id);
|
|
7876
|
+
}
|
|
7877
|
+
return ackIds;
|
|
7878
|
+
}
|
|
7879
|
+
function collectChatAckIds(evicted, existing = []) {
|
|
7880
|
+
const ackIds = serializeChatAckIds(existing);
|
|
7881
|
+
const seen = new Set(ackIds);
|
|
7882
|
+
for (const entry of Array.isArray(evicted) ? evicted : []) {
|
|
7883
|
+
const id = normalizePromptId(entry?.prompt_id);
|
|
7884
|
+
if (!id || seen.has(id)) continue;
|
|
7885
|
+
seen.add(id);
|
|
7886
|
+
ackIds.push(id);
|
|
7887
|
+
}
|
|
7888
|
+
return ackIds;
|
|
7889
|
+
}
|
|
7890
|
+
function serializeChatSync(session) {
|
|
7891
|
+
const revision = Number(session?.chat_revision);
|
|
7892
|
+
return {
|
|
7893
|
+
chat: serializeChat(session?.chat || []),
|
|
7894
|
+
ack_ids: serializeChatAckIds(session?.chat_ack_ids),
|
|
7895
|
+
chat_revision: Number.isSafeInteger(revision) && revision >= 0 ? revision : 0
|
|
7896
|
+
};
|
|
7897
|
+
}
|
|
7842
7898
|
|
|
7843
7899
|
// src/html-transform.js
|
|
7844
7900
|
function injectLavishSdk(html, key, artifactRevision, artifactLoadToken = "") {
|
|
@@ -8036,6 +8092,10 @@ var SessionStore = class {
|
|
|
8036
8092
|
delivered_attachments: Array.isArray(existing.delivered_attachments) ? existing.delivered_attachments : [],
|
|
8037
8093
|
dom_snapshot: existing.dom_snapshot || "",
|
|
8038
8094
|
chat: existing.chat || [],
|
|
8095
|
+
chat_revision: normalizeRevision(existing.chat_revision),
|
|
8096
|
+
// Compact prompt_id acks for bubbles evicted by the stored-chat byte bound. Reopening
|
|
8097
|
+
// must keep them: they are the settlement/dedup source once the visible entry is gone.
|
|
8098
|
+
chat_ack_ids: Array.isArray(existing.chat_ack_ids) ? existing.chat_ack_ids : [],
|
|
8039
8099
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8040
8100
|
};
|
|
8041
8101
|
state.sessions[key] = session;
|
|
@@ -8067,7 +8127,10 @@ var SessionStore = class {
|
|
|
8067
8127
|
let normalized = prompts.map(normalizePrompt);
|
|
8068
8128
|
if (!restoring) {
|
|
8069
8129
|
const acknowledgedIds = new Set(
|
|
8070
|
-
|
|
8130
|
+
[
|
|
8131
|
+
...(session.chat || []).map((entry) => normalizePromptId(entry?.prompt_id)),
|
|
8132
|
+
...(session.chat_ack_ids || []).map((id) => normalizePromptId(id))
|
|
8133
|
+
].filter(Boolean)
|
|
8071
8134
|
);
|
|
8072
8135
|
normalized = normalized.filter(({ prompt }) => {
|
|
8073
8136
|
const promptId = normalizePromptId(prompt.prompt_id);
|
|
@@ -8147,6 +8210,8 @@ var SessionStore = class {
|
|
|
8147
8210
|
const storedPrompts = restoring ? acceptedPrompts : acceptedPrompts.map(agentFacingPrompt);
|
|
8148
8211
|
session.prompts = restoring ? [...storedPrompts, ...existingPrompts] : [...existingPrompts, ...storedPrompts];
|
|
8149
8212
|
session.chat = [...session.chat || [], ...userMessages];
|
|
8213
|
+
applyTranscriptBound(session);
|
|
8214
|
+
if (userMessages.length > 0) session.chat_revision = normalizeRevision(session.chat_revision) + 1;
|
|
8150
8215
|
if (restoring) {
|
|
8151
8216
|
const restoredFailures = Array.isArray(payload.artifact_failures) ? JSON.parse(JSON.stringify(payload.artifact_failures)) : [];
|
|
8152
8217
|
const existingFailures = Array.isArray(session.artifact_failures) ? session.artifact_failures : [];
|
|
@@ -8467,11 +8532,11 @@ var SessionStore = class {
|
|
|
8467
8532
|
if (!session) {
|
|
8468
8533
|
return null;
|
|
8469
8534
|
}
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
session.updated_at =
|
|
8535
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
8536
|
+
session.chat = [...session.chat || [], { role: "agent", text: String(text || ""), at }];
|
|
8537
|
+
applyTranscriptBound(session);
|
|
8538
|
+
session.chat_revision = normalizeRevision(session.chat_revision) + 1;
|
|
8539
|
+
session.updated_at = at;
|
|
8475
8540
|
await this.writeState(state);
|
|
8476
8541
|
return session;
|
|
8477
8542
|
});
|
|
@@ -8515,7 +8580,15 @@ var SessionStore = class {
|
|
|
8515
8580
|
try {
|
|
8516
8581
|
const raw = await readFile3(this.file, "utf8");
|
|
8517
8582
|
const parsed = JSON.parse(raw);
|
|
8518
|
-
|
|
8583
|
+
const state = { sessions: parsed.sessions || {} };
|
|
8584
|
+
let changed = false;
|
|
8585
|
+
for (const session of Object.values(state.sessions)) {
|
|
8586
|
+
if (!session || typeof session !== "object" || !applyTranscriptBound(session)) continue;
|
|
8587
|
+
session.chat_revision = normalizeRevision(session.chat_revision) + 1;
|
|
8588
|
+
changed = true;
|
|
8589
|
+
}
|
|
8590
|
+
if (changed) await this.writeState(state);
|
|
8591
|
+
return state;
|
|
8519
8592
|
} catch (error) {
|
|
8520
8593
|
if (error && error.code === "ENOENT") {
|
|
8521
8594
|
return { sessions: {} };
|
|
@@ -8535,6 +8608,16 @@ async function canonicalFile(file) {
|
|
|
8535
8608
|
function sessionKey(file) {
|
|
8536
8609
|
return crypto4.createHash("sha256").update(file).digest("hex").slice(0, 16);
|
|
8537
8610
|
}
|
|
8611
|
+
function applyTranscriptBound(session) {
|
|
8612
|
+
const originalChat = Array.isArray(session.chat) ? session.chat : [];
|
|
8613
|
+
const { chat, evicted } = boundStoredChat(session.chat);
|
|
8614
|
+
const chatChanged = !Array.isArray(session.chat) || chat.length !== originalChat.length || chat.some((entry, index) => entry !== originalChat[index]);
|
|
8615
|
+
session.chat = chat;
|
|
8616
|
+
if (evicted.length === 0) return chatChanged;
|
|
8617
|
+
const existingAckCount = Array.isArray(session.chat_ack_ids) ? session.chat_ack_ids.length : 0;
|
|
8618
|
+
session.chat_ack_ids = collectChatAckIds(evicted, session.chat_ack_ids);
|
|
8619
|
+
return chatChanged || session.chat_ack_ids.length !== existingAckCount;
|
|
8620
|
+
}
|
|
8538
8621
|
function normalizePrompt(prompt) {
|
|
8539
8622
|
const normalized = {
|
|
8540
8623
|
uid: String(prompt.uid || ""),
|
|
@@ -9400,7 +9483,7 @@ async function serve({
|
|
|
9400
9483
|
}
|
|
9401
9484
|
events.on("reload", (key) => broadcastLiveEvent("reload", key));
|
|
9402
9485
|
events.on("agent-reply", (key, entry) => broadcastLiveEvent("agent-reply", key, entry));
|
|
9403
|
-
events.on("chat-sync", (key,
|
|
9486
|
+
events.on("chat-sync", (key, session) => broadcastLiveEvent("chat-sync", key, serializeChatSync(session)));
|
|
9404
9487
|
events.on("agent-presence", (key, state) => broadcastLiveEvent("agent-presence", key, { state }));
|
|
9405
9488
|
events.on("layout-warnings", (key, warnings) => broadcastLiveEvent("layout-warnings", key, { warnings }));
|
|
9406
9489
|
events.on("ended", (key, endedBy) => broadcastLiveEvent("ended", key, { ended_by: endedBy || null }));
|
|
@@ -9447,7 +9530,7 @@ async function serve({
|
|
|
9447
9530
|
cleanup();
|
|
9448
9531
|
return;
|
|
9449
9532
|
}
|
|
9450
|
-
client.sendEvent("chat-sync",
|
|
9533
|
+
client.sendEvent("chat-sync", serializeChatSync(session));
|
|
9451
9534
|
client.sendEvent("agent-presence", { state: computePresence(key, activePolls, deliveredFeedback) });
|
|
9452
9535
|
if (session?.status === "ended") client.sendEvent("ended", { ended_by: session.ended_by || null });
|
|
9453
9536
|
}
|
|
@@ -9475,11 +9558,8 @@ async function serve({
|
|
|
9475
9558
|
}
|
|
9476
9559
|
function finishFeedbackDelivery(key, result) {
|
|
9477
9560
|
if (result.status !== "feedback") return;
|
|
9478
|
-
const chat = result.chat;
|
|
9479
|
-
delete result.chat;
|
|
9480
9561
|
markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
|
|
9481
9562
|
if (result.session_ended) clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9482
|
-
if (Array.isArray(chat)) events.emit("chat-sync", key, chat);
|
|
9483
9563
|
}
|
|
9484
9564
|
async function restoreClosedFeedback(key, result) {
|
|
9485
9565
|
if (result.status !== "feedback") return;
|
|
@@ -9816,11 +9896,11 @@ async function serve({
|
|
|
9816
9896
|
}
|
|
9817
9897
|
if (shouldEndSession) events.emit("ended", req.params.key, publishedSession.ended_by);
|
|
9818
9898
|
else if (freshFeedback) events.emit("feedback", req.params.key, publishedSession.ended_by);
|
|
9819
|
-
events.emit("chat-sync", req.params.key, publishedSession
|
|
9899
|
+
events.emit("chat-sync", req.params.key, publishedSession);
|
|
9820
9900
|
res.json({
|
|
9821
9901
|
status: "queued",
|
|
9822
9902
|
pending_prompts: publishedSession.pending_prompts,
|
|
9823
|
-
|
|
9903
|
+
...serializeChatSync(publishedSession)
|
|
9824
9904
|
});
|
|
9825
9905
|
if (shouldEndSession) await shutdownIfNoLiveSessions();
|
|
9826
9906
|
} catch (error) {
|
|
@@ -9924,8 +10004,11 @@ async function serve({
|
|
|
9924
10004
|
res.status(404).json({ error: "session not found" });
|
|
9925
10005
|
return;
|
|
9926
10006
|
}
|
|
9927
|
-
const entry = serializeChat([
|
|
10007
|
+
const entry = serializeChat([
|
|
10008
|
+
session.chat?.at(-1)?.role === "agent" ? session.chat.at(-1) : { role: "agent", text, at: session.updated_at }
|
|
10009
|
+
])[0];
|
|
9928
10010
|
events.emit("agent-reply", req.params.key, entry);
|
|
10011
|
+
events.emit("chat-sync", req.params.key, session);
|
|
9929
10012
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
9930
10013
|
res.json({ status: "sent" });
|
|
9931
10014
|
} catch (error) {
|
|
@@ -11099,6 +11182,8 @@ function createChromeHtml(session, {
|
|
|
11099
11182
|
initialEnded: session.status === "ended",
|
|
11100
11183
|
initialEndedBy: session.ended_by || null,
|
|
11101
11184
|
initialChat: serializeChat(session.chat || []),
|
|
11185
|
+
initialChatAckIds: serializeChatAckIds(session.chat_ack_ids),
|
|
11186
|
+
initialChatRevision: Number.isSafeInteger(session.chat_revision) && session.chat_revision >= 0 ? session.chat_revision : 0,
|
|
11102
11187
|
// Bootstrapping the inbox from the server is what makes it survive a browser refresh or a
|
|
11103
11188
|
// reconnect: the chrome never owns warning state, it only renders it.
|
|
11104
11189
|
initialLayoutWarnings: serializeLayoutWarnings(session.layout_warnings),
|
|
@@ -11398,8 +11483,16 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
|
|
|
11398
11483
|
"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."
|
|
11399
11484
|
]);
|
|
11400
11485
|
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.";
|
|
11486
|
+
var POLL_AGENT_REPLY_RULE = "Keep the reply concise. Only when a longer reply is genuinely necessary, use Markdown structure - blank-line paragraphs, `- ` / `1. ` lists, `## ` headings - so it renders scannably instead of a wall of text, and pass that body with `--agent-reply-file <path>` (`-` reads stdin) so newlines survive quoting.";
|
|
11487
|
+
var POLL_AGENT_REPLY_HELP_POINTER = "The Conversation panel's Markdown subset is in README's Feedback controls bullet.";
|
|
11488
|
+
var POLL_AGENT_REPLY_NEXT_POINTER = "The Conversation panel's Markdown subset is in `lavish-axi poll --help` and README.";
|
|
11489
|
+
var POLL_VALUE_FLAGS = ["--agent-reply", "--agent-reply-file", "--timeout-ms"];
|
|
11490
|
+
var AGENT_REPLY_JSON_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
11491
|
+
var AGENT_REPLY_JSON_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ text: "" }));
|
|
11492
|
+
var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_JSON_ENVELOPE_BYTES;
|
|
11493
|
+
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
11401
11494
|
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.";
|
|
11402
|
-
var VERSION = "0.1.
|
|
11495
|
+
var VERSION = "0.1.71";
|
|
11403
11496
|
function detectInvokingAgent(env = process.env) {
|
|
11404
11497
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
11405
11498
|
}
|
|
@@ -11556,7 +11649,7 @@ function createOpenOutput({
|
|
|
11556
11649
|
session: { file, url, status },
|
|
11557
11650
|
...networkWarning ? { network_warning: networkWarning } : {},
|
|
11558
11651
|
...selfPaintWarning ? { self_paint_warning: selfPaintWarning } : {},
|
|
11559
|
-
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
|
|
11652
|
+
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 a concise response in Lavish Editor and wait for more feedback. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_NEXT_POINTER} 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\`.`
|
|
11560
11653
|
};
|
|
11561
11654
|
}
|
|
11562
11655
|
function createUserEndedOpenOutput({ file, url, networkWarning = void 0 }) {
|
|
@@ -11616,13 +11709,13 @@ function shouldOpenBrowser(args, env) {
|
|
|
11616
11709
|
return !args.includes("--no-open") && env.LAVISH_AXI_NO_OPEN !== "1";
|
|
11617
11710
|
}
|
|
11618
11711
|
async function pollCommand(args) {
|
|
11619
|
-
const file = firstPositionalArg(args,
|
|
11712
|
+
const file = firstPositionalArg(args, POLL_VALUE_FLAGS);
|
|
11620
11713
|
if (!file) {
|
|
11621
11714
|
throw new AxiError("HTML file path is required", "VALIDATION_ERROR", ["Run `lavish-axi poll <html-file>`"]);
|
|
11622
11715
|
}
|
|
11716
|
+
const agentReply = await resolveAgentReply(args);
|
|
11623
11717
|
const absolute = await canonicalFile(file);
|
|
11624
11718
|
const baseUrl = await ensureServer();
|
|
11625
|
-
const agentReply = flagValue(args, "--agent-reply");
|
|
11626
11719
|
if (agentReply) {
|
|
11627
11720
|
await postJson(`${baseUrl}/api/${sessionKey(absolute)}/agent-reply`, { text: agentReply });
|
|
11628
11721
|
}
|
|
@@ -11740,7 +11833,7 @@ function createFeedbackNextStep(file, artifactFailures, sessionEnded, endedBy, p
|
|
|
11740
11833
|
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.`;
|
|
11741
11834
|
}
|
|
11742
11835
|
const prefix = count > 0 ? artifactFailuresPrefix(file, artifactFailures) : `Apply the requested changes to ${file}. `;
|
|
11743
|
-
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 })}`;
|
|
11836
|
+
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. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_NEXT_POINTER} 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 })}`;
|
|
11744
11837
|
}
|
|
11745
11838
|
function artifactFailuresPrefix(file, artifactFailures) {
|
|
11746
11839
|
const count = artifactFailures.length;
|
|
@@ -12773,6 +12866,103 @@ function flagValue(args, flag) {
|
|
|
12773
12866
|
}
|
|
12774
12867
|
return null;
|
|
12775
12868
|
}
|
|
12869
|
+
function inspectValueFlag(args, flag) {
|
|
12870
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
12871
|
+
const arg = args[i];
|
|
12872
|
+
if (arg === "--") return { present: false };
|
|
12873
|
+
if (arg === flag) {
|
|
12874
|
+
return { present: true, value: i + 1 < args.length ? args[i + 1] : null, swallows: true };
|
|
12875
|
+
}
|
|
12876
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
12877
|
+
return { present: true, value: arg.slice(flag.length + 1), swallows: false };
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12880
|
+
return { present: false };
|
|
12881
|
+
}
|
|
12882
|
+
var AGENT_REPLY_FILE_HINT = "Pass --agent-reply-file <path>, or --agent-reply-file - to read stdin";
|
|
12883
|
+
function agentReplyTooLargeError() {
|
|
12884
|
+
return new AxiError(`Agent reply exceeds the ${AGENT_REPLY_LIMIT_LABEL}`, "VALIDATION_ERROR", [
|
|
12885
|
+
"Shorten the reply, then retry the same poll command"
|
|
12886
|
+
]);
|
|
12887
|
+
}
|
|
12888
|
+
async function readAgentReplyStream(stream) {
|
|
12889
|
+
const chunks = [];
|
|
12890
|
+
let bytes = 0;
|
|
12891
|
+
for await (const value of stream) {
|
|
12892
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
12893
|
+
bytes += chunk.length;
|
|
12894
|
+
if (bytes > AGENT_REPLY_INPUT_LIMIT_BYTES) {
|
|
12895
|
+
stream.destroy();
|
|
12896
|
+
throw agentReplyTooLargeError();
|
|
12897
|
+
}
|
|
12898
|
+
chunks.push(chunk);
|
|
12899
|
+
}
|
|
12900
|
+
const text = Buffer.concat(chunks, bytes).toString("utf8");
|
|
12901
|
+
if (Buffer.byteLength(JSON.stringify({ text })) > AGENT_REPLY_JSON_LIMIT_BYTES) {
|
|
12902
|
+
throw agentReplyTooLargeError();
|
|
12903
|
+
}
|
|
12904
|
+
return text;
|
|
12905
|
+
}
|
|
12906
|
+
async function resolveAgentReply(args, { createReadStreamFn = createReadStream, stdin = process.stdin, stdinIsTTY = process.stdin.isTTY === true } = {}) {
|
|
12907
|
+
const inline = inspectValueFlag(args, "--agent-reply");
|
|
12908
|
+
const fromFile = inspectValueFlag(args, "--agent-reply-file");
|
|
12909
|
+
if (inline.present && fromFile.present) {
|
|
12910
|
+
throw new AxiError("--agent-reply and --agent-reply-file cannot be combined", "VALIDATION_ERROR", [
|
|
12911
|
+
"Pass --agent-reply for a concise quoted reply, or --agent-reply-file <path> (`-` for stdin) when a longer Markdown body is necessary"
|
|
12912
|
+
]);
|
|
12913
|
+
}
|
|
12914
|
+
if (fromFile.present) {
|
|
12915
|
+
return readAgentReplyFile(fromFile, { createReadStreamFn, stdin, stdinIsTTY });
|
|
12916
|
+
}
|
|
12917
|
+
if (!inline.present) return null;
|
|
12918
|
+
return inline.value || null;
|
|
12919
|
+
}
|
|
12920
|
+
async function readAgentReplyFile(fromFile, { createReadStreamFn, stdin, stdinIsTTY }) {
|
|
12921
|
+
if (fromFile.swallows && typeof fromFile.value === "string" && fromFile.value.startsWith("--")) {
|
|
12922
|
+
throw new AxiError(
|
|
12923
|
+
`--agent-reply-file was given no value: the next argument ${fromFile.value} is another flag, so it would have been used as the path`,
|
|
12924
|
+
"VALIDATION_ERROR",
|
|
12925
|
+
[AGENT_REPLY_FILE_HINT, "Use --agent-reply-file=<path> if the path itself starts with --"]
|
|
12926
|
+
);
|
|
12927
|
+
}
|
|
12928
|
+
const spec = fromFile.value;
|
|
12929
|
+
if (spec == null || !String(spec).trim()) {
|
|
12930
|
+
throw new AxiError("--agent-reply-file was given an empty value", "VALIDATION_ERROR", [AGENT_REPLY_FILE_HINT]);
|
|
12931
|
+
}
|
|
12932
|
+
let text;
|
|
12933
|
+
if (spec === "-") {
|
|
12934
|
+
if (stdinIsTTY) {
|
|
12935
|
+
throw new AxiError("--agent-reply-file - cannot read stdin from a terminal", "VALIDATION_ERROR", [
|
|
12936
|
+
"Pipe a Markdown body into stdin, or pass --agent-reply-file <path>"
|
|
12937
|
+
]);
|
|
12938
|
+
}
|
|
12939
|
+
try {
|
|
12940
|
+
text = await readAgentReplyStream(stdin);
|
|
12941
|
+
} catch (error) {
|
|
12942
|
+
if (error instanceof AxiError) throw error;
|
|
12943
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
12944
|
+
throw new AxiError(`Cannot read --agent-reply-file stdin: ${detail}`, "VALIDATION_ERROR", [
|
|
12945
|
+
"Pipe a Markdown body into stdin, or pass --agent-reply-file <path>"
|
|
12946
|
+
]);
|
|
12947
|
+
}
|
|
12948
|
+
} else {
|
|
12949
|
+
try {
|
|
12950
|
+
text = await readAgentReplyStream(createReadStreamFn(spec));
|
|
12951
|
+
} catch (error) {
|
|
12952
|
+
if (error instanceof AxiError) throw error;
|
|
12953
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
12954
|
+
throw new AxiError(`Cannot read --agent-reply-file ${spec}: ${detail}`, "VALIDATION_ERROR", [
|
|
12955
|
+
"Pass a UTF-8 Markdown file, or `-` to read stdin"
|
|
12956
|
+
]);
|
|
12957
|
+
}
|
|
12958
|
+
}
|
|
12959
|
+
if (!String(text || "").trim()) {
|
|
12960
|
+
throw new AxiError("--agent-reply-file was empty", "VALIDATION_ERROR", [
|
|
12961
|
+
'Write Markdown into the file, or pass --agent-reply "<message>" for a concise reply'
|
|
12962
|
+
]);
|
|
12963
|
+
}
|
|
12964
|
+
return String(text);
|
|
12965
|
+
}
|
|
12776
12966
|
function isValueFlagToken(arg, flags) {
|
|
12777
12967
|
for (const flag of flags) {
|
|
12778
12968
|
if (arg === flag || arg.startsWith(`${flag}=`)) return true;
|
|
@@ -12791,7 +12981,7 @@ function createTopLevelHelp({ agent = "generic" } = {}) {
|
|
|
12791
12981
|
Usage:
|
|
12792
12982
|
lavish-axi
|
|
12793
12983
|
lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
12794
|
-
lavish-axi poll <html-file> [--agent-reply "..."]
|
|
12984
|
+
lavish-axi poll <html-file> [--agent-reply "..."] [--agent-reply-file <path>]
|
|
12795
12985
|
lavish-axi end <html-file>
|
|
12796
12986
|
lavish-axi export <html-file> [--out <path>]
|
|
12797
12987
|
lavish-axi share <html-file> [--private | --password <pw>] [--token <t>]
|
|
@@ -12815,9 +13005,16 @@ function createCommandHelp({ agent = "generic" } = {}) {
|
|
|
12815
13005
|
|
|
12816
13006
|
Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open. If the user explicitly ended the session from the browser, this refuses to reopen it and returns guidance instead - pass --reopen to force it open when the user asks for further review or something important needs their visual attention. Sessions ended by the agent (\`lavish-axi end\`) reopen normally without the flag.
|
|
12817
13007
|
`,
|
|
12818
|
-
poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."]
|
|
13008
|
+
poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."] [--agent-reply-file <path>]
|
|
13009
|
+
|
|
13010
|
+
This command long-polls indefinitely for queued user prompts. It stays silent while it waits - that is normal, never kill it. Browser-detected layout issues do NOT return this poll: they are filed passively in the user's Layout issues inbox and arrive as an ordinary tag "layout-warnings" prompt only after the user selects them and queues the fixes. Warning lifecycle: an issue stays unresolved and counted while queued, becomes recurring if a newer artifact revision still shows it, and is resolved only after a newer artifact load plus a complete diagnostic pass at the same viewport no longer detects it. A failed or incomplete pass preserves it as unverified rather than clearing it. The only response that arrives without user action is artifact_failures - a fatal failure that made the review surface itself unusable. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. ${pollExecutionGuidance({ agent })} Use --agent-reply after applying prior feedback to display a concise response in Lavish Editor before waiting again. ${POLL_AGENT_REPLY_RULE} ${POLL_AGENT_REPLY_HELP_POINTER} Do not combine --agent-reply with --agent-reply-file.
|
|
13011
|
+
|
|
13012
|
+
Examples:
|
|
13013
|
+
lavish-axi poll report.html --agent-reply "Renamed the payment step."
|
|
13014
|
+
lavish-axi poll report.html --agent-reply-file reply.md
|
|
13015
|
+
lavish-axi poll report.html --agent-reply-file -
|
|
12819
13016
|
|
|
12820
|
-
|
|
13017
|
+
${POLL_SEND_AND_END_RULE}
|
|
12821
13018
|
`,
|
|
12822
13019
|
end: `Usage: lavish-axi end <html-file>
|
|
12823
13020
|
|
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.71",
|
|
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",
|