lavish-axi 0.1.34 → 0.1.36
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 +15 -6
- package/dist/chrome-client.js +44 -8
- package/dist/cli.mjs +320 -37
- package/package.json +1 -1
- package/skills/lavish/SKILL.md +4 -3
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ HTML is the new markdown. Lavish is the new editor for your HTML artifacts.
|
|
|
31
31
|
Agents are good at producing rich HTML artifacts, but the human-agent collaboration loop on such artifacts is lacking and falls back into screenshots and long responses for “tell me what to change.”
|
|
32
32
|
That loses the thing HTML is best at: interactivity.
|
|
33
33
|
|
|
34
|
-
Lavish Editor opens agent-generated HTML files in a local browser, lets you pinpoint elements
|
|
34
|
+
Lavish Editor opens agent-generated HTML files in a local browser, lets you pinpoint elements, selected text, or Mermaid diagram nodes and send feedback to the agent to address.
|
|
35
35
|
|
|
36
36
|
- **Local-first** - Review local HTML artifacts with a local CLI and no cloud dependency in the core feedback loop; hosted sharing through third-party ht-ml.app is explicit and opt-in.
|
|
37
|
-
- **Human-AI collaboration** - Annotate elements, selected text ranges, and send messages to the agent without leaving Lavish Editor.
|
|
37
|
+
- **Human-AI collaboration** - Annotate elements, selected text ranges, and Mermaid diagram nodes, and send messages to the agent without leaving Lavish Editor.
|
|
38
38
|
- **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as product or technical plans, design explorations and more out of the box.
|
|
39
39
|
|
|
40
40
|
Lavish Editor is an [AXI](https://axi.md), which means -
|
|
@@ -54,6 +54,8 @@ npx skills add kunchenguid/lavish-axi --skill lavish
|
|
|
54
54
|
That is the entire setup - no npm install needed.
|
|
55
55
|
The skill teaches your agent to run Lavish through `npx -y lavish-axi`, so the CLI comes along on demand.
|
|
56
56
|
Its frontmatter also includes Hermes Agent metadata, so Hermes-compatible harnesses can categorize and surface it as a first-class productivity skill.
|
|
57
|
+
This installs the public `lavish` skill.
|
|
58
|
+
The repository also contains an internal `lavish-design` brand skill for maintainers; default `npx skills add ... --list` and skills.sh discovery hide it unless `INSTALL_INTERNAL_SKILLS=1` is set.
|
|
57
59
|
|
|
58
60
|
Then, in agents that expose skills as slash commands (Claude Code, for example), invoke it directly:
|
|
59
61
|
|
|
@@ -155,11 +157,17 @@ pnpm link
|
|
|
155
157
|
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
156
158
|
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()`.
|
|
157
159
|
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.
|
|
158
|
-
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which
|
|
160
|
+
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which sends the prompts and user-ended attribution together.
|
|
159
161
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
160
162
|
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.
|
|
163
|
+
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.
|
|
161
164
|
- **Agent presence** - The browser shows when no agent is listening, keeps queued feedback and fresh layout warnings for the next successful `lavish-axi poll` send even across reloads, and only blocks human sends while the agent is working on delivered feedback. The no-timeout poll writes an immediate stderr banner and periodic stderr heartbeats while stdout stays reserved for the final response; if the poll is interrupted or times out, re-run it because queued feedback is never lost.
|
|
165
|
+
- **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.
|
|
166
|
+
A plain `lavish-axi <html-file>` after a user-initiated end refuses to reopen the browser and returns guidance instead; pass `--reopen` only when the user asks for further review or something important needs their visual attention.
|
|
167
|
+
Agent-initiated ends keep reopening normally, same as before.
|
|
168
|
+
`lavish-axi poll`'s `ended` response and the `feedback` response for the final batch before an end both carry `next_step` guidance telling the agent to stop polling and deliver remaining updates in chat instead of reopening.
|
|
162
169
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
170
|
+
- **Mermaid diagrams** - Rendered Mermaid diagrams become pannable and zoomable while you explore (drag to pan, scroll to zoom) and freeze when you turn on annotation so a click lands on a single node. Clicking a node annotates the whole node and sends the agent its diagram id, node id, and rendered label instead of just a CSS selector. Lavish only enhances the live render, so the saved HTML still opens identically anywhere.
|
|
163
171
|
- **Server cleanup** - The detached server stops after the last session ends when nothing is connected, or after `LAVISH_AXI_IDLE_TIMEOUT_MS` (default 30 minutes) with no browser or poll connections.
|
|
164
172
|
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
165
173
|
- **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
|
|
@@ -171,9 +179,9 @@ pnpm link
|
|
|
171
179
|
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
180
|
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
173
181
|
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
174
|
-
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default.
|
|
175
|
-
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted.
|
|
176
|
-
| `lavish-axi end <html-file>` | End a session.
|
|
182
|
+
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
|
|
183
|
+
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. On `status: ended`, stop polling and do not reopen uninvited. |
|
|
184
|
+
| `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. |
|
|
177
185
|
| `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. |
|
|
178
186
|
| `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, and `--password` makes viewers enter the password before viewing. |
|
|
179
187
|
| `lavish-axi stop` | Shut down the background server. |
|
|
@@ -192,6 +200,7 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
192
200
|
| ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
193
201
|
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
194
202
|
| `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
|
|
203
|
+
| `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. |
|
|
195
204
|
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
196
205
|
| `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
|
|
197
206
|
| `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private; viewers must supply the password. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -7,6 +7,12 @@ const filePath = String(sessionData.file || "");
|
|
|
7
7
|
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
8
8
|
const internalQueueKeyField = "_lavishQueueKey";
|
|
9
9
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
10
|
+
const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
|
|
11
|
+
|
|
12
|
+
function isModeToggleHotkeyEvent(event) {
|
|
13
|
+
if (event.shiftKey || event.altKey) return false;
|
|
14
|
+
return Boolean(event.metaKey || event.ctrlKey) && String(event.key || "").toLowerCase() === MODE_TOGGLE_HOTKEY_KEY;
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
|
|
12
18
|
const annotationPills = /** @type {HTMLDivElement} */ (document.getElementById("annotationPills"));
|
|
@@ -328,21 +334,26 @@ async function submitQueued() {
|
|
|
328
334
|
submitQueuedAgain = false;
|
|
329
335
|
if (!succeeded) {
|
|
330
336
|
endAfterSubmit = false;
|
|
331
|
-
} else if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
endAfterSubmit
|
|
335
|
-
|
|
337
|
+
} else if (!ended && shouldSubmitAgain) {
|
|
338
|
+
if (queued.length) {
|
|
339
|
+
submitQueued();
|
|
340
|
+
} else if (endAfterSubmit) {
|
|
341
|
+
endAfterSubmit = false;
|
|
342
|
+
endSession();
|
|
343
|
+
}
|
|
336
344
|
}
|
|
337
345
|
}
|
|
338
346
|
}
|
|
339
347
|
|
|
340
348
|
async function submitQueuedOnce() {
|
|
341
349
|
const prompts = queued.slice();
|
|
350
|
+
const shouldEndSession = endAfterSubmit;
|
|
351
|
+
const body = { prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot };
|
|
352
|
+
if (shouldEndSession) body.endSession = true;
|
|
342
353
|
const response = await fetch("/api/" + key + "/prompts", {
|
|
343
354
|
method: "POST",
|
|
344
355
|
headers: { "content-type": "application/json" },
|
|
345
|
-
body: JSON.stringify(
|
|
356
|
+
body: JSON.stringify(body),
|
|
346
357
|
});
|
|
347
358
|
if (!response.ok) throw new Error("failed to submit queued prompts");
|
|
348
359
|
for (const prompt of prompts) {
|
|
@@ -351,6 +362,11 @@ async function submitQueuedOnce() {
|
|
|
351
362
|
}
|
|
352
363
|
persistQueuedPrompts();
|
|
353
364
|
render();
|
|
365
|
+
if (shouldEndSession) {
|
|
366
|
+
endAfterSubmit = false;
|
|
367
|
+
markSessionEnded();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
354
370
|
if (agentPresence === "listening") setAgentPresence("working");
|
|
355
371
|
}
|
|
356
372
|
|
|
@@ -474,6 +490,11 @@ async function endSession() {
|
|
|
474
490
|
if (ended) return;
|
|
475
491
|
const response = await fetch("/api/" + key + "/end", { method: "POST" });
|
|
476
492
|
if (!response.ok) throw new Error("failed to end session");
|
|
493
|
+
markSessionEnded();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function markSessionEnded() {
|
|
497
|
+
if (ended) return;
|
|
477
498
|
ended = true;
|
|
478
499
|
closeMenus();
|
|
479
500
|
annotationSwitch.disabled = true;
|
|
@@ -686,15 +707,19 @@ window.addEventListener("message", (event) => {
|
|
|
686
707
|
}
|
|
687
708
|
if (msg.type === "lavish:sendQueuedPrompts") sendQueued();
|
|
688
709
|
if (msg.type === "lavish:endSession") endSession();
|
|
710
|
+
if (msg.type === "lavish:toggleAnnotationMode") toggleAnnotationMode();
|
|
689
711
|
});
|
|
690
712
|
|
|
691
713
|
loadFrame();
|
|
692
714
|
|
|
693
|
-
|
|
715
|
+
function toggleAnnotationMode() {
|
|
716
|
+
if (ended) return;
|
|
694
717
|
annotation = !annotation;
|
|
695
718
|
annotationSwitch.setAttribute("aria-pressed", String(annotation));
|
|
696
719
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
|
|
697
|
-
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
annotationSwitch.onclick = toggleAnnotationMode;
|
|
698
723
|
|
|
699
724
|
sendButton.onclick = () => sendQueued(false);
|
|
700
725
|
sendFromMenuButton.onclick = () => sendQueued(false);
|
|
@@ -739,6 +764,17 @@ document.addEventListener("keydown", (event) => {
|
|
|
739
764
|
}
|
|
740
765
|
}
|
|
741
766
|
});
|
|
767
|
+
// Capture phase so the mode hotkey fires no matter where focus is in the chrome - including
|
|
768
|
+
// mid-keystroke in chatInput or an annotation-card textarea - without disturbing normal typing.
|
|
769
|
+
document.addEventListener(
|
|
770
|
+
"keydown",
|
|
771
|
+
(event) => {
|
|
772
|
+
if (!isModeToggleHotkeyEvent(event)) return;
|
|
773
|
+
event.preventDefault();
|
|
774
|
+
toggleAnnotationMode();
|
|
775
|
+
},
|
|
776
|
+
true,
|
|
777
|
+
);
|
|
742
778
|
frame.addEventListener("load", () => {
|
|
743
779
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation && !ended });
|
|
744
780
|
// Replay the pre-reload scroll position so hot reloads don't jump the artifact to the top.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
// src/cli.js
|
|
4
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -4188,7 +4193,67 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
4188
4193
|
import chokidar from "chokidar";
|
|
4189
4194
|
import express from "express";
|
|
4190
4195
|
|
|
4196
|
+
// src/mermaid-node.js
|
|
4197
|
+
var mermaid_node_exports = {};
|
|
4198
|
+
__export(mermaid_node_exports, {
|
|
4199
|
+
isMermaidSvg: () => isMermaidSvg,
|
|
4200
|
+
mermaidNodeElement: () => mermaidNodeElement,
|
|
4201
|
+
mermaidNodeFrom: () => mermaidNodeFrom,
|
|
4202
|
+
normalizeMermaidNodeTarget: () => normalizeMermaidNodeTarget,
|
|
4203
|
+
readNodeLabel: () => readNodeLabel
|
|
4204
|
+
});
|
|
4205
|
+
function isMermaidSvg(svg) {
|
|
4206
|
+
if (!svg) return false;
|
|
4207
|
+
const id = svg.id || "";
|
|
4208
|
+
if (id.startsWith("mermaid-") || id.startsWith("mermaid_")) return true;
|
|
4209
|
+
if (svg.getAttribute?.("aria-roledescription")) return true;
|
|
4210
|
+
return !!(svg.closest && svg.closest(".mermaid, [data-lavish-mermaid]"));
|
|
4211
|
+
}
|
|
4212
|
+
function readNodeLabel(labelEl) {
|
|
4213
|
+
if (!labelEl) return "";
|
|
4214
|
+
let source = labelEl;
|
|
4215
|
+
if (labelEl.querySelector?.("br") && labelEl.cloneNode) {
|
|
4216
|
+
source = labelEl.cloneNode(true);
|
|
4217
|
+
for (const br of source.querySelectorAll("br")) br.replaceWith(document.createTextNode(" "));
|
|
4218
|
+
}
|
|
4219
|
+
return (source.textContent || "").trim().replace(/\s+/g, " ").slice(0, 120);
|
|
4220
|
+
}
|
|
4221
|
+
function mermaidNodeElement(el) {
|
|
4222
|
+
if (!el || !el.closest) return null;
|
|
4223
|
+
const node = el.closest("g.node, g.nodes > g");
|
|
4224
|
+
if (!node) return null;
|
|
4225
|
+
const svg = node.closest("svg");
|
|
4226
|
+
return svg && isMermaidSvg(svg) ? node : null;
|
|
4227
|
+
}
|
|
4228
|
+
function mermaidNodeFrom(el, selector) {
|
|
4229
|
+
const node = mermaidNodeElement(el);
|
|
4230
|
+
if (!node) return null;
|
|
4231
|
+
const svg = node.closest("svg");
|
|
4232
|
+
const labelEl = node.querySelector(".nodeLabel, .label, foreignObject span, text");
|
|
4233
|
+
return {
|
|
4234
|
+
type: "mermaid-node",
|
|
4235
|
+
diagramId: svg.id || "",
|
|
4236
|
+
nodeId: node.id || "",
|
|
4237
|
+
label: readNodeLabel(labelEl),
|
|
4238
|
+
selector: typeof selector === "function" ? selector(node) : ""
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function normalizeMermaidNodeTarget(target) {
|
|
4242
|
+
return {
|
|
4243
|
+
type: "mermaid-node",
|
|
4244
|
+
diagramId: String(target.diagramId || ""),
|
|
4245
|
+
nodeId: String(target.nodeId || ""),
|
|
4246
|
+
label: String(target.label || ""),
|
|
4247
|
+
selector: String(target.selector || "")
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4191
4251
|
// src/artifact-sdk.js
|
|
4252
|
+
var MODE_TOGGLE_HOTKEY_KEY = "i";
|
|
4253
|
+
function isModeToggleHotkeyEvent(event) {
|
|
4254
|
+
if (event.shiftKey || event.altKey) return false;
|
|
4255
|
+
return Boolean(event.metaKey || event.ctrlKey) && String(event.key || "").toLowerCase() === MODE_TOGGLE_HOTKEY_KEY;
|
|
4256
|
+
}
|
|
4192
4257
|
function deriveLavishQueueKey(element, options = {}) {
|
|
4193
4258
|
function stringValue(value) {
|
|
4194
4259
|
return value === null || value === void 0 ? "" : String(value);
|
|
@@ -4328,7 +4393,8 @@ function resolveVisibleSpillCandidates(spillCandidates, { epsilon = 1 } = {}) {
|
|
|
4328
4393
|
)
|
|
4329
4394
|
);
|
|
4330
4395
|
}
|
|
4331
|
-
function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInteractiveControl) {
|
|
4396
|
+
function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInteractiveControl, mermaid = mermaid_node_exports) {
|
|
4397
|
+
const { isMermaidSvg: isMermaidSvg2, mermaidNodeFrom: mermaidNodeFrom2, mermaidNodeElement: mermaidNodeElement2 } = mermaid;
|
|
4332
4398
|
let annotationMode = true;
|
|
4333
4399
|
let hovered = null;
|
|
4334
4400
|
let selected = null;
|
|
@@ -4340,6 +4406,12 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4340
4406
|
if (!ids.has(el)) ids.set(el, String(++counter));
|
|
4341
4407
|
return ids.get(el);
|
|
4342
4408
|
}
|
|
4409
|
+
function escapeAnnotationText(value) {
|
|
4410
|
+
return String(value).replace(
|
|
4411
|
+
/[&<>"']/g,
|
|
4412
|
+
(char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4343
4415
|
function selector(el) {
|
|
4344
4416
|
if (!el || !el.tagName) return "";
|
|
4345
4417
|
const parts = [];
|
|
@@ -4362,12 +4434,138 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4362
4434
|
return parts.join(" > ");
|
|
4363
4435
|
}
|
|
4364
4436
|
function context(el) {
|
|
4365
|
-
|
|
4437
|
+
const base = {
|
|
4366
4438
|
uid: uid(el),
|
|
4367
4439
|
selector: selector(el),
|
|
4368
4440
|
tag: (el.tagName || "").toLowerCase(),
|
|
4369
4441
|
text: (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 240)
|
|
4370
4442
|
};
|
|
4443
|
+
const mermaidNode = mermaidNodeFrom2(el, selector);
|
|
4444
|
+
if (mermaidNode) {
|
|
4445
|
+
base.tag = "mermaid-node";
|
|
4446
|
+
base.text = mermaidNode.label || base.text;
|
|
4447
|
+
base.target = mermaidNode;
|
|
4448
|
+
}
|
|
4449
|
+
return base;
|
|
4450
|
+
}
|
|
4451
|
+
function annotationTargetEl(el) {
|
|
4452
|
+
return mermaidNodeElement2(el) || el;
|
|
4453
|
+
}
|
|
4454
|
+
const mermaidViewports = /* @__PURE__ */ new WeakMap();
|
|
4455
|
+
function findMermaidSvgs() {
|
|
4456
|
+
const svgs = /* @__PURE__ */ new Set();
|
|
4457
|
+
for (const svg of document.querySelectorAll("svg")) {
|
|
4458
|
+
if (isMermaidSvg2(svg)) svgs.add(svg);
|
|
4459
|
+
}
|
|
4460
|
+
return [...svgs];
|
|
4461
|
+
}
|
|
4462
|
+
function createViewport(svg) {
|
|
4463
|
+
const bbox = svg.getBBox ? safeBBox(svg) : null;
|
|
4464
|
+
const initial = readViewBox(svg) || (bbox ? { x: bbox.x, y: bbox.y, w: bbox.width, h: bbox.height } : null);
|
|
4465
|
+
if (!initial) return null;
|
|
4466
|
+
svg.setAttribute("viewBox", `${initial.x} ${initial.y} ${initial.w} ${initial.h}`);
|
|
4467
|
+
const view = { ...initial };
|
|
4468
|
+
let frozen = false;
|
|
4469
|
+
let panning = null;
|
|
4470
|
+
function apply() {
|
|
4471
|
+
svg.setAttribute("viewBox", `${view.x} ${view.y} ${view.w} ${view.h}`);
|
|
4472
|
+
}
|
|
4473
|
+
function reset() {
|
|
4474
|
+
Object.assign(view, initial);
|
|
4475
|
+
apply();
|
|
4476
|
+
}
|
|
4477
|
+
function zoomAt(clientX, clientY, factor) {
|
|
4478
|
+
const rect = svg.getBoundingClientRect();
|
|
4479
|
+
if (!rect.width || !rect.height) return;
|
|
4480
|
+
const px = (clientX - rect.left) / rect.width;
|
|
4481
|
+
const py = (clientY - rect.top) / rect.height;
|
|
4482
|
+
const fx = view.x + view.w * px;
|
|
4483
|
+
const fy = view.y + view.h * py;
|
|
4484
|
+
const next = Math.min(Math.max(view.w * factor, initial.w / 40), initial.w * 8);
|
|
4485
|
+
const scale = next / view.w;
|
|
4486
|
+
view.w = next;
|
|
4487
|
+
view.h *= scale;
|
|
4488
|
+
view.x = fx - (fx - view.x) * scale;
|
|
4489
|
+
view.y = fy - (fy - view.y) * scale;
|
|
4490
|
+
apply();
|
|
4491
|
+
}
|
|
4492
|
+
function onWheel(event) {
|
|
4493
|
+
if (frozen) return;
|
|
4494
|
+
event.preventDefault();
|
|
4495
|
+
zoomAt(event.clientX, event.clientY, event.deltaY > 0 ? 1.15 : 1 / 1.15);
|
|
4496
|
+
}
|
|
4497
|
+
function onPointerDown(event) {
|
|
4498
|
+
if (frozen || event.button !== 0) return;
|
|
4499
|
+
panning = { x: event.clientX, y: event.clientY, vx: view.x, vy: view.y };
|
|
4500
|
+
svg.setPointerCapture?.(event.pointerId);
|
|
4501
|
+
svg.style.cursor = "grabbing";
|
|
4502
|
+
}
|
|
4503
|
+
function onPointerMove(event) {
|
|
4504
|
+
if (!panning) return;
|
|
4505
|
+
const rect = svg.getBoundingClientRect();
|
|
4506
|
+
if (!rect.width || !rect.height) return;
|
|
4507
|
+
view.x = panning.vx - (event.clientX - panning.x) / rect.width * view.w;
|
|
4508
|
+
view.y = panning.vy - (event.clientY - panning.y) / rect.height * view.h;
|
|
4509
|
+
apply();
|
|
4510
|
+
}
|
|
4511
|
+
function onPointerUp(event) {
|
|
4512
|
+
panning = null;
|
|
4513
|
+
svg.releasePointerCapture?.(event.pointerId);
|
|
4514
|
+
svg.style.cursor = frozen ? "" : "grab";
|
|
4515
|
+
}
|
|
4516
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
4517
|
+
svg.addEventListener("pointerdown", onPointerDown);
|
|
4518
|
+
svg.addEventListener("pointermove", onPointerMove);
|
|
4519
|
+
svg.addEventListener("pointerup", onPointerUp);
|
|
4520
|
+
svg.addEventListener("pointercancel", onPointerUp);
|
|
4521
|
+
function setFrozen(next) {
|
|
4522
|
+
frozen = !!next;
|
|
4523
|
+
panning = null;
|
|
4524
|
+
svg.style.cursor = frozen ? "" : "grab";
|
|
4525
|
+
svg.style.touchAction = frozen ? "" : "none";
|
|
4526
|
+
}
|
|
4527
|
+
setFrozen(false);
|
|
4528
|
+
return { reset, setFrozen };
|
|
4529
|
+
}
|
|
4530
|
+
function safeBBox(svg) {
|
|
4531
|
+
try {
|
|
4532
|
+
return svg.getBBox();
|
|
4533
|
+
} catch {
|
|
4534
|
+
return null;
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
function readViewBox(svg) {
|
|
4538
|
+
const raw = svg.getAttribute?.("viewBox");
|
|
4539
|
+
if (!raw) return null;
|
|
4540
|
+
const parts = raw.trim().split(/[\s,]+/).map(Number);
|
|
4541
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return null;
|
|
4542
|
+
return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] };
|
|
4543
|
+
}
|
|
4544
|
+
function enhanceMermaid() {
|
|
4545
|
+
for (const svg of findMermaidSvgs()) {
|
|
4546
|
+
if (mermaidViewports.has(svg)) continue;
|
|
4547
|
+
const viewport = createViewport(svg);
|
|
4548
|
+
if (viewport) {
|
|
4549
|
+
viewport.setFrozen(annotationMode);
|
|
4550
|
+
mermaidViewports.set(svg, viewport);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
let mermaidEnhanceScheduled = false;
|
|
4555
|
+
function scheduleMermaidEnhance() {
|
|
4556
|
+
if (mermaidEnhanceScheduled) return;
|
|
4557
|
+
mermaidEnhanceScheduled = true;
|
|
4558
|
+
const run2 = () => {
|
|
4559
|
+
mermaidEnhanceScheduled = false;
|
|
4560
|
+
enhanceMermaid();
|
|
4561
|
+
};
|
|
4562
|
+
if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(run2);
|
|
4563
|
+
else window.setTimeout(run2, 50);
|
|
4564
|
+
}
|
|
4565
|
+
function setMermaidFrozen(frozen) {
|
|
4566
|
+
for (const svg of findMermaidSvgs()) {
|
|
4567
|
+
mermaidViewports.get(svg)?.setFrozen(frozen);
|
|
4568
|
+
}
|
|
4371
4569
|
}
|
|
4372
4570
|
function closestElement(node) {
|
|
4373
4571
|
if (!node) return document.body;
|
|
@@ -4465,6 +4663,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4465
4663
|
}
|
|
4466
4664
|
if (!annotationMode && style) style.remove();
|
|
4467
4665
|
if (!annotationMode) closeCard();
|
|
4666
|
+
setMermaidFrozen(annotationMode);
|
|
4468
4667
|
}
|
|
4469
4668
|
function queuePrompt(prompt, options = {}) {
|
|
4470
4669
|
const originElement = options.element || document.activeElement || document.body;
|
|
@@ -4832,17 +5031,20 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4832
5031
|
const root = ensureShadow();
|
|
4833
5032
|
closeCard();
|
|
4834
5033
|
const c = options.context || context(target);
|
|
5034
|
+
let anchor = target;
|
|
4835
5035
|
if (options.range) {
|
|
4836
5036
|
highlightTextRange(options.range);
|
|
4837
5037
|
} else {
|
|
4838
|
-
|
|
5038
|
+
anchor = annotationTargetEl(target);
|
|
5039
|
+
selected = anchor;
|
|
4839
5040
|
highlightElement(selected);
|
|
4840
5041
|
}
|
|
4841
|
-
const rect = options.range ? options.range.getBoundingClientRect() :
|
|
5042
|
+
const rect = options.range ? options.range.getBoundingClientRect() : anchor.getBoundingClientRect();
|
|
4842
5043
|
const card = document.createElement("div");
|
|
4843
5044
|
card.className = "lavish-annotation-card";
|
|
4844
|
-
const
|
|
4845
|
-
const
|
|
5045
|
+
const nodeLabel = c.tag === "mermaid-node" ? c.target?.label || c.text || "" : "";
|
|
5046
|
+
const heading = c.tag === "text" ? "Annotate text" : c.tag === "mermaid-node" ? "Annotate node" + (nodeLabel ? ": " + escapeAnnotationText(nodeLabel) : "") : "Annotate <" + c.tag + ">";
|
|
5047
|
+
const placeholder = c.tag === "text" ? "Tell the agent what to change about this text..." : c.tag === "mermaid-node" ? "Tell the agent what to change about this diagram node..." : "Tell the agent what to change about this element...";
|
|
4846
5048
|
card.innerHTML = '<div class="lavish-heading">' + heading + '</div><textarea placeholder="' + placeholder + '"></textarea><div class="lavish-hint">Enter to queue · ' + (/Mac|iP(hone|ad|od)/.test(navigator.platform) ? "\u2318" : "Ctrl") + '+Enter to send now</div><div class="lavish-row"><button class="lavish-cancel" type="button">Cancel</button><button class="lavish-send" type="button">Queue</button></div>';
|
|
4847
5049
|
root.appendChild(card);
|
|
4848
5050
|
const left = Math.min(Math.max(12, rect.left), window.innerWidth - card.offsetWidth - 12);
|
|
@@ -4896,6 +5098,15 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4896
5098
|
window.scrollTo(Number(msg.x) || 0, Number(msg.y) || 0);
|
|
4897
5099
|
}
|
|
4898
5100
|
});
|
|
5101
|
+
document.addEventListener(
|
|
5102
|
+
"keydown",
|
|
5103
|
+
(event) => {
|
|
5104
|
+
if (!isModeToggleHotkeyEvent(event)) return;
|
|
5105
|
+
event.preventDefault();
|
|
5106
|
+
parent.postMessage({ type: "lavish:toggleAnnotationMode" }, "*");
|
|
5107
|
+
},
|
|
5108
|
+
true
|
|
5109
|
+
);
|
|
4899
5110
|
let scrollFrame = 0;
|
|
4900
5111
|
window.addEventListener(
|
|
4901
5112
|
"scroll",
|
|
@@ -4913,9 +5124,10 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4913
5124
|
(event) => {
|
|
4914
5125
|
if (!annotationMode || isLavishUi(event.target) || isLavishAction(event.target) || isInteractiveControl(event.target))
|
|
4915
5126
|
return;
|
|
4916
|
-
|
|
5127
|
+
const target = annotationTargetEl(event.target);
|
|
5128
|
+
if (target === selected) return;
|
|
4917
5129
|
if (hovered && hovered !== selected) clearHighlight(hovered);
|
|
4918
|
-
hovered =
|
|
5130
|
+
hovered = target;
|
|
4919
5131
|
highlightElement(hovered);
|
|
4920
5132
|
},
|
|
4921
5133
|
true
|
|
@@ -4963,6 +5175,12 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4963
5175
|
} else {
|
|
4964
5176
|
startLayoutAudit();
|
|
4965
5177
|
}
|
|
5178
|
+
enhanceMermaid();
|
|
5179
|
+
if (document.readyState === "loading") {
|
|
5180
|
+
document.addEventListener("DOMContentLoaded", enhanceMermaid, { once: true });
|
|
5181
|
+
}
|
|
5182
|
+
const mermaidObserver = new MutationObserver(() => scheduleMermaidEnhance());
|
|
5183
|
+
mermaidObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
4966
5184
|
}
|
|
4967
5185
|
|
|
4968
5186
|
// src/html-transform.js
|
|
@@ -5027,13 +5245,16 @@ var SessionStore = class {
|
|
|
5027
5245
|
return null;
|
|
5028
5246
|
}
|
|
5029
5247
|
const prompts = Array.isArray(payload.prompts) ? payload.prompts : [];
|
|
5248
|
+
const shouldEndSession = Boolean(payload.endSession || payload.end_session);
|
|
5249
|
+
const alreadyEnded = session.status === "ended";
|
|
5030
5250
|
const normalizedPrompts = prompts.map(normalizePrompt);
|
|
5031
5251
|
const userMessages = normalizedPrompts.filter((prompt) => prompt.tag === "message" && prompt.prompt).map((prompt) => ({ role: "user", text: prompt.prompt, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
5032
5252
|
session.prompts = [...session.prompts || [], ...normalizedPrompts];
|
|
5033
5253
|
session.chat = [...session.chat || [], ...userMessages];
|
|
5034
5254
|
session.pending_prompts = session.prompts.length;
|
|
5035
5255
|
session.dom_snapshot = String(payload.domSnapshot || payload.dom_snapshot || "");
|
|
5036
|
-
session.status = "feedback";
|
|
5256
|
+
session.status = shouldEndSession || alreadyEnded ? "ended" : "feedback";
|
|
5257
|
+
if (shouldEndSession) session.ended_by = "user";
|
|
5037
5258
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5038
5259
|
await this.writeState(state);
|
|
5039
5260
|
return session;
|
|
@@ -5078,14 +5299,18 @@ var SessionStore = class {
|
|
|
5078
5299
|
}
|
|
5079
5300
|
const prompts = session.prompts || [];
|
|
5080
5301
|
const layoutWarnings = session.layout_warnings || [];
|
|
5302
|
+
const alreadyEnded = session.status === "ended";
|
|
5081
5303
|
if (prompts.length === 0 && layoutWarnings.length === 0) {
|
|
5082
|
-
return
|
|
5304
|
+
return alreadyEnded ? { status: "ended", ended_by: session.ended_by } : { status: "waiting" };
|
|
5083
5305
|
}
|
|
5084
5306
|
const result = {
|
|
5085
5307
|
status: "feedback",
|
|
5086
5308
|
dom_snapshot: session.dom_snapshot || "",
|
|
5087
5309
|
prompts,
|
|
5088
|
-
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {}
|
|
5310
|
+
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
|
|
5311
|
+
// This is the final delivery before the session shows as ended - flag it so the agent
|
|
5312
|
+
// knows not to expect (or force) a reopened browser afterward.
|
|
5313
|
+
...alreadyEnded ? { session_ended: true, ended_by: session.ended_by } : {}
|
|
5089
5314
|
};
|
|
5090
5315
|
session.prompts = [];
|
|
5091
5316
|
session.layout_warnings = [];
|
|
@@ -5096,20 +5321,26 @@ var SessionStore = class {
|
|
|
5096
5321
|
for (const warning of layoutWarnings) deliveredKeys.add(layoutWarningKey(warning));
|
|
5097
5322
|
session.delivered_layout_warning_keys = [...deliveredKeys].slice(-200);
|
|
5098
5323
|
}
|
|
5099
|
-
if (
|
|
5324
|
+
if (!alreadyEnded) {
|
|
5100
5325
|
session.status = "open";
|
|
5101
5326
|
}
|
|
5102
5327
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5103
5328
|
await this.writeState(state);
|
|
5104
5329
|
return result;
|
|
5105
5330
|
}
|
|
5106
|
-
|
|
5331
|
+
// `endedBy` distinguishes a human ending review from the browser chrome ("user") from an
|
|
5332
|
+
// agent explicitly closing the loop via `lavish-axi end` ("agent"). Only a user-initiated end
|
|
5333
|
+
// blocks a plain reopen - see `SessionStore` callers in server.js.
|
|
5334
|
+
async endSession(key, endedBy = "agent") {
|
|
5107
5335
|
const state = await this.readState();
|
|
5108
5336
|
const session = state.sessions[key];
|
|
5109
5337
|
if (!session) {
|
|
5110
5338
|
return null;
|
|
5111
5339
|
}
|
|
5340
|
+
const existingEndedBy = session.status === "ended" ? session.ended_by : void 0;
|
|
5341
|
+
const nextEndedBy = endedBy === "user" || existingEndedBy === "user" ? "user" : "agent";
|
|
5112
5342
|
session.status = "ended";
|
|
5343
|
+
session.ended_by = nextEndedBy;
|
|
5113
5344
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5114
5345
|
await this.writeState(state);
|
|
5115
5346
|
return session;
|
|
@@ -5185,6 +5416,7 @@ function normalizeFiniteNumber(value) {
|
|
|
5185
5416
|
}
|
|
5186
5417
|
function normalizeTarget(target) {
|
|
5187
5418
|
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
5419
|
+
if (target.type === "mermaid-node") return normalizeMermaidNodeTarget(target);
|
|
5188
5420
|
return JSON.parse(JSON.stringify(target));
|
|
5189
5421
|
}
|
|
5190
5422
|
|
|
@@ -5256,9 +5488,15 @@ async function serve({
|
|
|
5256
5488
|
try {
|
|
5257
5489
|
const file = await canonicalFile(req.body.file);
|
|
5258
5490
|
const key = sessionKey(file);
|
|
5491
|
+
const reopen = Boolean(req.body.reopen);
|
|
5492
|
+
const existing = await store.findByKey(key);
|
|
5493
|
+
if (existing?.status === "ended" && existing.ended_by === "user" && !reopen) {
|
|
5494
|
+
logEvent?.(`session open blocked (user-ended) key=${key} file=${file}`);
|
|
5495
|
+
res.json({ key, file, url: existing.url, status: "user-ended" });
|
|
5496
|
+
return;
|
|
5497
|
+
}
|
|
5259
5498
|
const sessionUrl = `http://${hostForUrl(linkHostName)}:${publicPort}/session/${key}`;
|
|
5260
5499
|
const url = shouldDisableLayoutGateOpen(req.body || {}) ? appendNoGateParam(sessionUrl) : sessionUrl;
|
|
5261
|
-
const existing = await store.findByKey(key);
|
|
5262
5500
|
const session = await store.upsertSession(file, sessionUrl);
|
|
5263
5501
|
if (existing?.status === "ended") {
|
|
5264
5502
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
@@ -5344,13 +5582,16 @@ async function serve({
|
|
|
5344
5582
|
});
|
|
5345
5583
|
app.post("/api/:key/prompts", async (req, res, next) => {
|
|
5346
5584
|
try {
|
|
5585
|
+
const shouldEndSession = Boolean(req.body?.endSession || req.body?.end_session);
|
|
5347
5586
|
const session = await store.queuePrompts(req.params.key, req.body || {});
|
|
5348
5587
|
if (!session) {
|
|
5349
5588
|
res.status(404).json({ error: "session not found" });
|
|
5350
5589
|
return;
|
|
5351
5590
|
}
|
|
5352
|
-
|
|
5591
|
+
if (shouldEndSession) clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
5592
|
+
events.emit(shouldEndSession ? "ended" : "feedback", req.params.key);
|
|
5353
5593
|
res.json({ status: "queued", pending_prompts: session.pending_prompts });
|
|
5594
|
+
if (shouldEndSession) await shutdownIfNoLiveSessions();
|
|
5354
5595
|
} catch (error) {
|
|
5355
5596
|
next(error);
|
|
5356
5597
|
}
|
|
@@ -5372,7 +5613,7 @@ async function serve({
|
|
|
5372
5613
|
});
|
|
5373
5614
|
app.post("/api/:key/end", async (req, res, next) => {
|
|
5374
5615
|
try {
|
|
5375
|
-
await store.endSession(req.params.key);
|
|
5616
|
+
await store.endSession(req.params.key, "user");
|
|
5376
5617
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
5377
5618
|
events.emit("ended", req.params.key);
|
|
5378
5619
|
res.json({ status: "ended" });
|
|
@@ -5459,7 +5700,7 @@ async function serve({
|
|
|
5459
5700
|
try {
|
|
5460
5701
|
const file = await canonicalFile(req.body.file);
|
|
5461
5702
|
const key = sessionKey(file);
|
|
5462
|
-
await store.endSession(key);
|
|
5703
|
+
await store.endSession(key, "agent");
|
|
5463
5704
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
5464
5705
|
events.emit("ended", key);
|
|
5465
5706
|
res.json({ status: "ended" });
|
|
@@ -5905,11 +6146,14 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5905
6146
|
key: session.key,
|
|
5906
6147
|
file: session.file,
|
|
5907
6148
|
initialChat: session.chat || [],
|
|
5908
|
-
layoutGateEnabled
|
|
6149
|
+
layoutGateEnabled,
|
|
6150
|
+
modeToggleHotkeyKey: MODE_TOGGLE_HOTKEY_KEY
|
|
5909
6151
|
});
|
|
5910
6152
|
const { head: pathHead, tail: pathTail } = displayPathParts(session.file);
|
|
5911
6153
|
const bodyClass = layoutGateEnabled ? "lavish layout-gate-active" : "lavish";
|
|
5912
6154
|
const layoutGateHidden = layoutGateEnabled ? "" : " hidden";
|
|
6155
|
+
const modeHotkeyUpper = MODE_TOGGLE_HOTKEY_KEY.toUpperCase();
|
|
6156
|
+
const modeToggleHint = `Toggle annotate/explore mode (\u2318${modeHotkeyUpper} / Ctrl+${modeHotkeyUpper})`;
|
|
5913
6157
|
return `<!doctype html>
|
|
5914
6158
|
<html>
|
|
5915
6159
|
<head>
|
|
@@ -5919,7 +6163,7 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5919
6163
|
<link rel="stylesheet" href="/chrome.css">
|
|
5920
6164
|
</head>
|
|
5921
6165
|
<body class="${bodyClass}">
|
|
5922
|
-
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="spacer" aria-hidden="true"></div><button class="annotate-switch" id="annotation" type="button" aria-pressed="true"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml(pathHead)}</span><span class="path-tail">${escapeHtml(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
6166
|
+
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="spacer" aria-hidden="true"></div><button class="annotate-switch" id="annotation" type="button" aria-pressed="true" title="${escapeHtml(modeToggleHint)}"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml(pathHead)}</span><span class="path-tail">${escapeHtml(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
5923
6167
|
<div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe><div class="layout-issue-banner" id="layoutIssueBanner" hidden>This surface may have layout issues. Your agent has been notified.</div></div><aside class="panel"><h2>Conversation</h2><div class="chat" id="chatLog"></div><div class="composer"><div class="presence-banner" id="presenceBanner" hidden>Your agent is not listening. If this persists, ask your agent to poll for updates from Lavish.</div><div class="annotation-pills" id="annotationPills"></div><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="actions" id="sendActions"><span class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</span><div class="split"><button class="button send-main" id="send">Send to Agent</button><button class="button send-caret" id="sendCaret" type="button" title="Send options" aria-haspopup="menu" aria-expanded="false">${chromeIcons.caret}</button></div><div class="menu send-menu" id="sendMenu" hidden><button class="menu-item" id="sendFromMenu" type="button">${chromeIcons.send}<span>Send to Agent</span></button><button class="menu-item danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send & end session</span></button></div></div></div></aside></div>
|
|
5924
6168
|
<div class="share-overlay" id="shareDialog" role="dialog" aria-modal="true" aria-labelledby="shareTitleText" hidden><form class="share-card" id="shareForm"><div class="share-head"><div><div class="share-kicker">Publish to <a class="share-link" href="https://ht-ml.app" target="_blank" rel="noopener noreferrer">ht-ml.app</a></div><h2 id="shareTitleText">Publish artifact</h2></div><button class="share-close" id="shareClose" type="button" aria-label="Close publish dialog"><svg width="14" height="14" 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></button></div><p class="share-note">ht-ml.app is a separate, third-party hosting service, not part of Lavish. Publishing sends this artifact to its servers.</p><p class="share-copy">This uploads this artifact to ht-ml.app with local assets inlined. Without a password, the page is PUBLIC and anyone with the link can open it. With a password, the page is PRIVATE and viewers must supply the password to view.</p><p class="share-note">Do not publish secrets. The Lavish annotation SDK is not included.</p><div class="share-grid"><label>Password (optional)<input id="sharePassword" name="password" type="password" autocomplete="new-password" placeholder="Leave blank for a public page"></label></div><div class="share-status" id="shareStatus" role="status"></div><div class="share-result" id="shareResult" hidden><label>Share URL<div class="share-copy-row"><input id="shareUrl" readonly><button class="share-copy-btn" id="copyShareUrl" type="button">Copy URL</button></div></label><label>Update key (secret)<div class="share-copy-row"><input id="shareUpdateKey" readonly><button class="share-copy-btn" id="copyUpdateKey" type="button">Copy key</button></div></label><p class="share-note">Keep the update key private. ht-ml.app returns it once and it is the only way to update or delete this page later.</p></div><div class="share-actions"><button class="share-cancel" id="shareCancel" type="button">Cancel</button><button class="button" id="sharePublish" type="submit">Publish</button></div></form></div>
|
|
5925
6169
|
<div class="ended-overlay layout-gate-overlay" id="layoutGateOverlay"${layoutGateHidden}><div class="ended-card"><div class="ended-title" id="layoutGateTitle">Checking layout.<br>One moment.</div><p class="ended-copy" id="layoutGateCopy">Lavish is waiting for fonts and final geometry before revealing this artifact.</p><button class="button ended-action" id="layoutGateAction" type="button">Show anyway</button></div></div>
|
|
@@ -5930,16 +6174,23 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5930
6174
|
</html>`;
|
|
5931
6175
|
}
|
|
5932
6176
|
function createSdkJs(key) {
|
|
6177
|
+
const mermaidHelperEntries = Object.entries(mermaid_node_exports).filter(([, value]) => typeof value === "function");
|
|
6178
|
+
const mermaidHelperDecls = mermaidHelperEntries.map(([name, fn]) => `const ${name}=${fn.toString()};`).join("\n");
|
|
6179
|
+
const mermaidHelperKeys = mermaidHelperEntries.map(([name]) => name).join(", ");
|
|
5933
6180
|
return `(() => {
|
|
5934
6181
|
const key=${JSON.stringify(key)};
|
|
5935
6182
|
void key;
|
|
5936
6183
|
const deriveQueueKey=${deriveLavishQueueKey.toString()};
|
|
5937
6184
|
const isNativeInteractiveControl=${isNativeInteractiveControl.toString()};
|
|
6185
|
+
const MODE_TOGGLE_HOTKEY_KEY=${JSON.stringify(MODE_TOGGLE_HOTKEY_KEY)};
|
|
6186
|
+
const isModeToggleHotkeyEvent=${isModeToggleHotkeyEvent.toString()};
|
|
5938
6187
|
const fragmentsSignificantlyOverlap=${fragmentsSignificantlyOverlap.toString()};
|
|
5939
6188
|
const resolveVisibleSpillCandidates=${resolveVisibleSpillCandidates.toString()};
|
|
5940
6189
|
const classifyHorizontalOverflow=${classifyHorizontalOverflow.toString()};
|
|
5941
6190
|
const classifyVerticalOverflow=${classifyVerticalOverflow.toString()};
|
|
5942
|
-
|
|
6191
|
+
${mermaidHelperDecls}
|
|
6192
|
+
const mermaidHelpers={ ${mermaidHelperKeys} };
|
|
6193
|
+
(${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl, mermaidHelpers);
|
|
5943
6194
|
})();`;
|
|
5944
6195
|
}
|
|
5945
6196
|
function escapeHtml(value) {
|
|
@@ -6125,7 +6376,7 @@ function normalizePagePath(path6) {
|
|
|
6125
6376
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
|
|
6126
6377
|
var RESERVED = new Set(RESERVED_COMMANDS);
|
|
6127
6378
|
var DESCRIPTION = "Lavish Editor helps agents turn rich HTML artifacts into collaborative human review surfaces. Whenever you are about to give user a complex response that will be easier to understand via a rich / interactive page, consider using Lavish Editor. First generate an interactive HTML artifact according to user request, then run `lavish-axi <html-file>` so the user can visually review it, annotate elements or selected text, queue prompts, and send feedback back through `lavish-axi poll`.";
|
|
6128
|
-
var VERSION = "0.1.
|
|
6379
|
+
var VERSION = "0.1.36";
|
|
6129
6380
|
async function run(argv) {
|
|
6130
6381
|
await ensureStateDir();
|
|
6131
6382
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -6217,11 +6468,11 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
|
|
|
6217
6468
|
],
|
|
6218
6469
|
playbooks: listPlaybooks(),
|
|
6219
6470
|
help: [
|
|
6220
|
-
"Run `lavish-axi <html-file>` to open or resume a Lavish Editor session",
|
|
6471
|
+
"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",
|
|
6221
6472
|
"Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
|
|
6222
6473
|
"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",
|
|
6223
|
-
"Run `lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost",
|
|
6224
|
-
"Run `lavish-axi end <html-file>` to end a session",
|
|
6474
|
+
"Run `lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead",
|
|
6475
|
+
"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`",
|
|
6225
6476
|
"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",
|
|
6226
6477
|
"Run `lavish-axi share <html-file> [--password <pw>] [--token <t>]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu",
|
|
6227
6478
|
"Run `lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)",
|
|
@@ -6250,7 +6501,13 @@ function createPlaybookOutput(args) {
|
|
|
6250
6501
|
function createOpenOutput({ file, url, status }) {
|
|
6251
6502
|
return {
|
|
6252
6503
|
session: { file, url, status },
|
|
6253
|
-
next_step: `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 the real browser reports layout_warnings from the in-iframe layout audit, and it stays silent the whole time - that is normal, never kill it. If layout_warnings arrive, follow the poll response's next_step: fix and re-check fresh error-severity overflow or clipped-text findings before involving the human, but persistent or low-severity warnings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if the poll still gets killed or times out, just re-run it - queued feedback is never lost. 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
|
|
6504
|
+
next_step: `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 the real browser reports layout_warnings from the in-iframe layout audit, and it stays silent the whole time - that is normal, never kill it. If layout_warnings arrive, follow the poll response's next_step: fix and re-check fresh error-severity overflow or clipped-text findings before involving the human, but persistent or low-severity warnings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if the poll still gets killed or times out, just re-run it - queued feedback is never lost. 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\`.`
|
|
6505
|
+
};
|
|
6506
|
+
}
|
|
6507
|
+
function createUserEndedOpenOutput({ file, url }) {
|
|
6508
|
+
return {
|
|
6509
|
+
session: { file, url, status: "user-ended" },
|
|
6510
|
+
next_step: `The user explicitly ended this Lavish Editor session from the browser, so \`lavish-axi ${file}\` did not reopen it. Do not reopen 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\`.`
|
|
6254
6511
|
};
|
|
6255
6512
|
}
|
|
6256
6513
|
async function openCommand(args) {
|
|
@@ -6261,8 +6518,12 @@ async function openCommand(args) {
|
|
|
6261
6518
|
await assertHtmlFile(file);
|
|
6262
6519
|
const absolute = await canonicalFile(file);
|
|
6263
6520
|
const noGate = args.includes("--no-gate");
|
|
6521
|
+
const reopen = args.includes("--reopen");
|
|
6264
6522
|
const baseUrl = await ensureServer({ forceRestart: shouldForceRestartForLocalBuild(process.argv[1] || "") });
|
|
6265
|
-
const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate });
|
|
6523
|
+
const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate, reopen });
|
|
6524
|
+
if (response.status === "user-ended") {
|
|
6525
|
+
return createUserEndedOpenOutput({ file: absolute, url: response.url });
|
|
6526
|
+
}
|
|
6266
6527
|
if (shouldOpenBrowser(args, process.env)) {
|
|
6267
6528
|
try {
|
|
6268
6529
|
const open = (await import("open")).default;
|
|
@@ -6350,24 +6611,40 @@ function createPollOutput({ file, response }) {
|
|
|
6350
6611
|
}
|
|
6351
6612
|
if (response.status === "feedback") {
|
|
6352
6613
|
const layoutWarnings = Array.isArray(response.layout_warnings) ? response.layout_warnings : [];
|
|
6614
|
+
const sessionEnded = Boolean(response.session_ended);
|
|
6615
|
+
const endedBy = typeof response.ended_by === "string" ? response.ended_by : void 0;
|
|
6353
6616
|
return {
|
|
6354
|
-
session: {
|
|
6617
|
+
session: {
|
|
6618
|
+
file,
|
|
6619
|
+
status: "feedback",
|
|
6620
|
+
...sessionEnded ? { session_ended: true, ...endedBy ? { ended_by: endedBy } : {} } : {}
|
|
6621
|
+
},
|
|
6355
6622
|
dom_snapshot: response.dom_snapshot || "",
|
|
6356
6623
|
prompts: response.prompts || [],
|
|
6357
6624
|
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
|
|
6358
|
-
next_step: createFeedbackNextStep(file, layoutWarnings)
|
|
6625
|
+
next_step: createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy)
|
|
6359
6626
|
};
|
|
6360
6627
|
}
|
|
6361
6628
|
if (response.status === "ended") {
|
|
6362
|
-
return {
|
|
6629
|
+
return {
|
|
6630
|
+
session: { file, status: "ended", ...response.ended_by ? { ended_by: response.ended_by } : {} },
|
|
6631
|
+
next_step: createEndedNextStep(file, response.ended_by)
|
|
6632
|
+
};
|
|
6363
6633
|
}
|
|
6364
6634
|
return {
|
|
6365
6635
|
session: { file, status: response.status || "waiting" },
|
|
6366
6636
|
next_step: `No user feedback arrived before the optional timeout. Run \`lavish-axi poll ${file}\` without --timeout-ms to wait indefinitely - queued feedback is never lost, so re-running the poll is always safe.`
|
|
6367
6637
|
};
|
|
6368
6638
|
}
|
|
6369
|
-
function createFeedbackNextStep(file, layoutWarnings) {
|
|
6639
|
+
function createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy) {
|
|
6370
6640
|
const count = layoutWarnings.length;
|
|
6641
|
+
if (sessionEnded) {
|
|
6642
|
+
const layoutNote = count > 0 ? `${count} layout warning${count === 1 ? "" : "s"} arrived alongside this final feedback. ` : "";
|
|
6643
|
+
if (endedBy === "user") {
|
|
6644
|
+
return `${layoutNote}This was the last feedback before the user ended the session. Stop polling ${file} and do not reopen it - deliver any remaining updates directly in this conversation instead. Only run \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
|
|
6645
|
+
}
|
|
6646
|
+
return `${layoutNote}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.`;
|
|
6647
|
+
}
|
|
6371
6648
|
const layoutPrefix = count > 0 ? layoutWarningsPrefix(file, layoutWarnings) : `Apply the requested changes to ${file}. `;
|
|
6372
6649
|
return `${layoutPrefix}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 reports fresh layout_warnings - never kill it. If your harness limits how long a foreground command may run, run the poll as a background task; if it still gets killed or times out, just re-run it - queued feedback is never lost.`;
|
|
6373
6650
|
}
|
|
@@ -6386,7 +6663,13 @@ function layoutWarningsPrefix(file, layoutWarnings) {
|
|
|
6386
6663
|
if (allRepeatOrLowSeverity) {
|
|
6387
6664
|
return `${count} layout warning${plural} detected, with no fresh error-severity findings - fix any obvious low-severity issue in ${file}, otherwise it is fine to proceed to the human with a note instead of iterating further. `;
|
|
6388
6665
|
}
|
|
6389
|
-
return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then
|
|
6666
|
+
return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then re-check in the browser before involving the human. Lavish live-reloads the artifact automatically after you save, so you do not need to re-run \`lavish-axi ${file}\` for this. `;
|
|
6667
|
+
}
|
|
6668
|
+
function createEndedNextStep(file, endedBy) {
|
|
6669
|
+
if (endedBy === "user") {
|
|
6670
|
+
return `The user ended this Lavish Editor session. Stop polling ${file} - do not run \`lavish-axi ${file}\` to reopen it. Deliver any remaining updates directly in this conversation instead. Only reopen with \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
|
|
6671
|
+
}
|
|
6672
|
+
return `This Lavish Editor session for ${file} has ended. Stop polling. 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.`;
|
|
6390
6673
|
}
|
|
6391
6674
|
async function endCommand(args) {
|
|
6392
6675
|
const file = firstPositionalArg(args);
|
|
@@ -6910,7 +7193,7 @@ var TOP_LEVEL_HELP = `lavish-axi - Lavish Editor AXI
|
|
|
6910
7193
|
|
|
6911
7194
|
Usage:
|
|
6912
7195
|
lavish-axi
|
|
6913
|
-
lavish-axi <html-file> [--no-open] [--no-gate]
|
|
7196
|
+
lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
6914
7197
|
lavish-axi poll <html-file> [--agent-reply "..."]
|
|
6915
7198
|
lavish-axi end <html-file>
|
|
6916
7199
|
lavish-axi export <html-file> [--out <path>]
|
|
@@ -6922,21 +7205,21 @@ Usage:
|
|
|
6922
7205
|
|
|
6923
7206
|
${DESIGN_SYSTEM_HINT}
|
|
6924
7207
|
|
|
6925
|
-
Note: poll long-polls indefinitely by default until the user sends feedback, ends the session, or the browser reports fresh layout_warnings, staying silent while it waits - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost.
|
|
7208
|
+
Note: poll long-polls indefinitely by default until the user sends feedback, ends the session, or the browser reports fresh layout_warnings, staying silent while it waits - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When the user ends a session from the browser, stop polling and do not reopen it uninvited - pass --reopen to <html-file> only when the user asks for further review or something important needs their visual attention.
|
|
6926
7209
|
|
|
6927
7210
|
`;
|
|
6928
7211
|
var COMMAND_HELP = {
|
|
6929
|
-
open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate]
|
|
7212
|
+
open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
6930
7213
|
|
|
6931
|
-
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.
|
|
7214
|
+
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.
|
|
6932
7215
|
`,
|
|
6933
7216
|
poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."]
|
|
6934
7217
|
|
|
6935
|
-
This command long-polls indefinitely for queued user prompts and browser-reported layout_warnings, then returns them to the agent. It stays silent while it waits - that is normal, never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if it still gets killed or times out, just re-run it - queued feedback is never lost. Use --agent-reply after applying prior feedback to display your response in Lavish Editor before waiting again.
|
|
7218
|
+
This command long-polls indefinitely for queued user prompts and browser-reported layout_warnings, then returns them to the agent. It stays silent while it waits - that is normal, never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if it still gets killed or times out, just re-run it - queued feedback is never lost. Use --agent-reply after applying prior feedback to display your response in Lavish Editor before waiting again. When status is ended, stop polling and do not reopen the session uninvited - deliver remaining updates directly in this conversation instead.
|
|
6936
7219
|
`,
|
|
6937
7220
|
end: `Usage: lavish-axi end <html-file>
|
|
6938
7221
|
|
|
6939
|
-
End a Lavish Editor session.
|
|
7222
|
+
End a Lavish Editor session as the agent. A session ended this way still reopens normally on the next \`lavish-axi <html-file>\`, unlike a user ending it from the browser, which requires --reopen.
|
|
6940
7223
|
`,
|
|
6941
7224
|
export: `Usage: lavish-axi export <html-file> [--out <path>]
|
|
6942
7225
|
|
package/package.json
CHANGED
package/skills/lavish/SKILL.md
CHANGED
|
@@ -37,6 +37,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac
|
|
|
37
37
|
4. If poll returns `layout_warnings`, follow the returned `next_step`: fix and re-check fresh error-severity findings, but proceed with a note instead of looping when every current warning is persistent or low-severity.
|
|
38
38
|
5. Apply human feedback, then poll again with `--agent-reply "<message>"` to reply in the browser and keep the loop going.
|
|
39
39
|
6. Run `npx -y lavish-axi end <html-file>` when the review is finished.
|
|
40
|
+
7. If the user ends the session from the browser instead, `npx -y lavish-axi <html-file>` refuses to reopen it and says so - only pass `--reopen` when the user asks for further review or something genuinely important needs their visual attention. Otherwise deliver remaining updates directly in this conversation.
|
|
40
41
|
|
|
41
42
|
## Visual guidance
|
|
42
43
|
|
|
@@ -62,11 +63,11 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an
|
|
|
62
63
|
|
|
63
64
|
## Commands & rules
|
|
64
65
|
|
|
65
|
-
- Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session
|
|
66
|
+
- Run `npx -y 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
|
|
66
67
|
- Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`
|
|
67
68
|
- 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
|
|
68
|
-
- Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost
|
|
69
|
-
- Run `npx -y lavish-axi end <html-file>` to end a session
|
|
69
|
+
- Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead
|
|
70
|
+
- Run `npx -y 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 `npx -y lavish-axi <html-file>` refuses to reopen it without `--reopen`
|
|
70
71
|
- Run `npx -y 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
|
|
71
72
|
- Run `npx -y lavish-axi share <html-file> [--password <pw>] [--token <t>]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu
|
|
72
73
|
- Run `npx -y lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)
|