lavish-axi 0.1.30 → 0.1.32
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 +32 -20
- package/dist/chrome-client.js +183 -6
- package/dist/chrome.css +28 -0
- package/dist/cli.mjs +860 -354
- package/package.json +2 -2
- package/skills/lavish/SKILL.md +11 -9
package/README.md
CHANGED
|
@@ -117,8 +117,9 @@ pnpm link
|
|
|
117
117
|
▼
|
|
118
118
|
┌────────────────────────┐
|
|
119
119
|
│ Human annotates text │
|
|
120
|
-
│ or elements,
|
|
121
|
-
│
|
|
120
|
+
│ or elements, sends │
|
|
121
|
+
│ chat, or browser audit │
|
|
122
|
+
│ reports layout issues │
|
|
122
123
|
└───────┬────────────────┘
|
|
123
124
|
▼
|
|
124
125
|
┌────────────────────────┐
|
|
@@ -128,20 +129,27 @@ pnpm link
|
|
|
128
129
|
```
|
|
129
130
|
|
|
130
131
|
- **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
|
|
131
|
-
- **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and
|
|
132
|
+
- **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, feedback controls, and render-time layout checks.
|
|
132
133
|
Lavish does not inject any design system, so the saved HTML file renders identically whether you open it through `lavish-axi` or directly in a browser.
|
|
133
134
|
Before writing HTML, choose a design system in strict priority order: follow a user-requested look first; otherwise inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's Tailwind or theme config, CSS variables or design tokens, component library, brand assets, or existing styled pages.
|
|
134
135
|
If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo.
|
|
135
|
-
Only when both come up empty, run `lavish-axi design` for a copy-pasteable Tailwind CSS v4 + DaisyUI v5 CDN fallback.
|
|
136
|
-
That fallback guidance recommends DaisyUI's `luxury` theme by default
|
|
136
|
+
Only when both come up empty, run `lavish-axi design` for a copy-pasteable Tailwind CSS v4 + DaisyUI v5 CDN fallback, a content-to-playbook router, and Mermaid diagram tooling.
|
|
137
|
+
That fallback guidance recommends DaisyUI's `luxury` theme by default, warns not to `@apply` DaisyUI classes inside Tailwind browser-runtime style blocks, includes an optional layout safety CSS snippet for dense nested grid/flex layouts, and provides a pinned Mermaid CDN snippet with initialization for flows, architecture, state, and sequence diagrams.
|
|
138
|
+
- **Open-time layout gate** - The browser chrome masks each artifact until the real in-iframe layout audit reports no error-severity findings.
|
|
139
|
+
Warning-only artifacts reveal normally; error findings notify the agent through the same `layout_warnings` poll path and keep the curtain up until a clean reload.
|
|
140
|
+
The user can click **Show anyway**, and a bounded safety timeout reveals with a persistent layout-issues banner so review is never blocked indefinitely.
|
|
141
|
+
- **Layout warnings** - After fonts load and layout settles, the injected SDK audits the real browser render for page horizontal overflow, element overflow, clipped text, and overlapping text.
|
|
142
|
+
Intentional horizontal scrollers using `overflow-x: auto` or `scroll` are excluded.
|
|
143
|
+
Fresh warnings are returned from `lavish-axi poll` as `layout_warnings` with `selector`, `kind`, `overflowPx`, `viewportWidth`, and `severity`, so agents can fix unreadable layouts before asking the human to review.
|
|
137
144
|
- **Local assets** - Copy local images, CSS, fonts, and scripts next to the HTML artifact and reference them with relative paths from that directory; root-prefixed paths such as `/assets/logo.png` will not resolve through Lavish's artifact route.
|
|
138
145
|
- **Live reload** - Lavish watches the HTML artifact file by default and preserves the artifact iframe scroll position across reloads. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
|
|
139
|
-
- **Feedback controls** - Native
|
|
140
|
-
|
|
146
|
+
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
147
|
+
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()`.
|
|
148
|
+
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.
|
|
141
149
|
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
|
|
142
150
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
143
151
|
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.
|
|
144
|
-
- **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 only blocks
|
|
152
|
+
- **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.
|
|
145
153
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
146
154
|
- **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.
|
|
147
155
|
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
@@ -150,26 +158,30 @@ pnpm link
|
|
|
150
158
|
|
|
151
159
|
## CLI Reference
|
|
152
160
|
|
|
153
|
-
| Command | Description
|
|
154
|
-
| ----------------------------- |
|
|
155
|
-
| `lavish-axi` | Show current sessions and usage guidance.
|
|
156
|
-
| `lavish-axi
|
|
157
|
-
| `lavish-axi
|
|
158
|
-
| `lavish-axi
|
|
159
|
-
| `lavish-axi
|
|
160
|
-
| `lavish-axi
|
|
161
|
-
| `lavish-axi
|
|
162
|
-
| `lavish-axi
|
|
163
|
-
| `lavish-axi
|
|
161
|
+
| Command | Description |
|
|
162
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
164
|
+
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
165
|
+
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. |
|
|
166
|
+
| `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. |
|
|
167
|
+
| `lavish-axi end <html-file>` | End a session. |
|
|
168
|
+
| `lavish-axi stop` | Shut down the background server. |
|
|
169
|
+
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
|
|
170
|
+
| `lavish-axi design` | Show the Tailwind + DaisyUI CDN fallback, content-to-playbook router, Mermaid diagram tooling, `luxury` default theme, DaisyUI `@apply` warning, and layout safety snippet. |
|
|
171
|
+
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, and OpenCode; restart the agent session afterward. |
|
|
172
|
+
| `lavish-axi server` | Run the local Lavish Editor server. |
|
|
164
173
|
|
|
165
174
|
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
|
|
166
|
-
One artifact often combines several playbooks, such as a plan that includes a comparison and a diagram, so
|
|
175
|
+
One artifact often combines several playbooks, such as a plan that includes a comparison and a diagram, so agents must match against each `use_when` trigger and open every matching playbook before writing HTML.
|
|
176
|
+
For flows, architecture, state, or sequence diagrams, open the diagram playbook and use the Mermaid tooling from `lavish-axi design` unless SVG is needed for richly annotated nodes; avoid hand-built div/flexbox boxes-and-arrows.
|
|
167
177
|
|
|
168
178
|
### Flags
|
|
169
179
|
|
|
170
180
|
| Command | Flag | Description |
|
|
171
181
|
| ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
172
182
|
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
183
|
+
| `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
|
|
184
|
+
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
173
185
|
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
|
|
174
186
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
|
175
187
|
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -5,6 +5,7 @@ const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
|
|
|
5
5
|
const key = String(sessionData.key || "");
|
|
6
6
|
const filePath = String(sessionData.file || "");
|
|
7
7
|
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
8
|
+
const internalQueueKeyField = "_lavishQueueKey";
|
|
8
9
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
9
10
|
|
|
10
11
|
const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
|
|
@@ -29,13 +30,31 @@ const copyHint = /** @type {HTMLSpanElement} */ (document.getElementById("copyHi
|
|
|
29
30
|
const copyHintText = /** @type {HTMLSpanElement} */ (document.getElementById("copyHintText"));
|
|
30
31
|
const presenceBanner = /** @type {HTMLDivElement} */ (document.getElementById("presenceBanner"));
|
|
31
32
|
const endedOverlay = /** @type {HTMLDivElement} */ (document.getElementById("endedOverlay"));
|
|
33
|
+
const layoutGateOverlay = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateOverlay"));
|
|
34
|
+
const layoutGateTitle = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateTitle"));
|
|
35
|
+
const layoutGateCopy = /** @type {HTMLParagraphElement} */ (document.getElementById("layoutGateCopy"));
|
|
36
|
+
const layoutGateAction = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateAction"));
|
|
37
|
+
const layoutIssueBanner = /** @type {HTMLDivElement} */ (document.getElementById("layoutIssueBanner"));
|
|
32
38
|
const sendHint = /** @type {HTMLSpanElement} */ (document.getElementById("sendHint"));
|
|
39
|
+
const artifactSrc = frame.dataset.artifactSrc || frame.getAttribute?.("data-artifact-src") || frame.src || "";
|
|
33
40
|
|
|
34
41
|
const queued = loadQueuedPrompts();
|
|
35
42
|
let annotation = true;
|
|
36
43
|
let ended = false;
|
|
37
44
|
let agentPresence = "waiting";
|
|
38
45
|
let pendingSnapshot = "";
|
|
46
|
+
const layoutGateEnabled = sessionData.layoutGateEnabled !== false;
|
|
47
|
+
const configuredLayoutGateMaxHoldMs = Number(sessionData.layoutGateMaxHoldMs);
|
|
48
|
+
const layoutGateMaxHoldMs =
|
|
49
|
+
Number.isFinite(configuredLayoutGateMaxHoldMs) && configuredLayoutGateMaxHoldMs > 0
|
|
50
|
+
? Math.min(configuredLayoutGateMaxHoldMs, 60_000)
|
|
51
|
+
: 12_000;
|
|
52
|
+
let layoutGateVisible = false;
|
|
53
|
+
let layoutGateArmed = false;
|
|
54
|
+
let layoutGateManuallyBypassed = !layoutGateEnabled;
|
|
55
|
+
let layoutGateCycle = 0;
|
|
56
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
57
|
+
let layoutGateTimer;
|
|
39
58
|
const snapshotRequests = [];
|
|
40
59
|
let endAfterSubmit = false;
|
|
41
60
|
let workingBubble = null;
|
|
@@ -212,6 +231,36 @@ function removeQueuedPrompt(index, event) {
|
|
|
212
231
|
render();
|
|
213
232
|
}
|
|
214
233
|
|
|
234
|
+
function promptQueueKey(prompt) {
|
|
235
|
+
return prompt && typeof prompt[internalQueueKeyField] === "string" ? prompt[internalQueueKeyField].trim() : "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function enqueuePrompt(prompt) {
|
|
239
|
+
if (!prompt || typeof prompt !== "object") return;
|
|
240
|
+
|
|
241
|
+
const queueKey = promptQueueKey(prompt);
|
|
242
|
+
if (queueKey) {
|
|
243
|
+
const index = queued.findIndex((item) => promptQueueKey(item) === queueKey);
|
|
244
|
+
if (index !== -1) {
|
|
245
|
+
queued[index] = prompt;
|
|
246
|
+
} else {
|
|
247
|
+
queued.push(prompt);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
queued.push(prompt);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
persistQueuedPrompts();
|
|
254
|
+
render();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function stripInternalPromptFields(prompt) {
|
|
258
|
+
if (!prompt || typeof prompt !== "object") return prompt;
|
|
259
|
+
const clean = { ...prompt };
|
|
260
|
+
delete clean[internalQueueKeyField];
|
|
261
|
+
return clean;
|
|
262
|
+
}
|
|
263
|
+
|
|
215
264
|
function postToFrame(message) {
|
|
216
265
|
if (frame.contentWindow) frame.contentWindow.postMessage(message, "*");
|
|
217
266
|
}
|
|
@@ -279,7 +328,7 @@ async function submitQueuedOnce() {
|
|
|
279
328
|
const response = await fetch("/api/" + key + "/prompts", {
|
|
280
329
|
method: "POST",
|
|
281
330
|
headers: { "content-type": "application/json" },
|
|
282
|
-
body: JSON.stringify({ prompts, domSnapshot: pendingSnapshot }),
|
|
331
|
+
body: JSON.stringify({ prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot }),
|
|
283
332
|
});
|
|
284
333
|
if (!response.ok) throw new Error("failed to submit queued prompts");
|
|
285
334
|
for (const prompt of prompts) {
|
|
@@ -291,6 +340,122 @@ async function submitQueuedOnce() {
|
|
|
291
340
|
if (agentPresence === "listening") setAgentPresence("working");
|
|
292
341
|
}
|
|
293
342
|
|
|
343
|
+
function normalizeLayoutWarningsPayload(value) {
|
|
344
|
+
return Array.isArray(value) ? value.filter((item) => item && typeof item === "object") : [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isErrorLayoutWarning(warning) {
|
|
348
|
+
return String(warning?.severity || "").toLowerCase() === "error";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function setLayoutIssueBanner(visible, text = "This surface may have layout issues. Your agent has been notified.") {
|
|
352
|
+
if (!layoutIssueBanner) return;
|
|
353
|
+
layoutIssueBanner.textContent = text;
|
|
354
|
+
layoutIssueBanner.hidden = !visible;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clearLayoutGateTimer() {
|
|
358
|
+
if (layoutGateTimer) clearTimeout(layoutGateTimer);
|
|
359
|
+
layoutGateTimer = undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function setLayoutGateCard(state) {
|
|
363
|
+
if (!layoutGateTitle || !layoutGateCopy) return;
|
|
364
|
+
|
|
365
|
+
if (state === "held") {
|
|
366
|
+
layoutGateTitle.innerHTML = "Fixing a layout issue...";
|
|
367
|
+
layoutGateCopy.textContent =
|
|
368
|
+
"The real browser found overflow or overlapping content. Your agent has been notified and this will reveal after the next clean reload.";
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
layoutGateTitle.innerHTML = "Checking layout.<br>One moment.";
|
|
373
|
+
layoutGateCopy.textContent = "Lavish is waiting for fonts and final geometry before revealing this artifact.";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function setLayoutGateActive(active) {
|
|
377
|
+
layoutGateVisible = active;
|
|
378
|
+
if (layoutGateOverlay) layoutGateOverlay.hidden = !active;
|
|
379
|
+
document.body?.classList?.toggle("layout-gate-active", active);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function revealLayoutGate({ showBanner = false, bannerText = undefined } = {}) {
|
|
383
|
+
clearLayoutGateTimer();
|
|
384
|
+
layoutGateArmed = false;
|
|
385
|
+
setLayoutGateActive(false);
|
|
386
|
+
setLayoutIssueBanner(showBanner, bannerText);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function forceRevealLayoutGate(reason) {
|
|
390
|
+
if (!layoutGateEnabled || ended) return;
|
|
391
|
+
if (reason === "manual") layoutGateManuallyBypassed = true;
|
|
392
|
+
const bannerText =
|
|
393
|
+
reason === "timeout"
|
|
394
|
+
? "This surface may have layout issues. Lavish revealed it after the safety timeout so review is never blocked."
|
|
395
|
+
: "This surface may have layout issues. You chose to show it before the layout check passed.";
|
|
396
|
+
revealLayoutGate({ showBanner: true, bannerText });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function startLayoutGateCycle() {
|
|
400
|
+
if (!layoutGateEnabled || layoutGateManuallyBypassed || ended) return;
|
|
401
|
+
|
|
402
|
+
layoutGateCycle += 1;
|
|
403
|
+
layoutGateArmed = true;
|
|
404
|
+
setLayoutIssueBanner(false);
|
|
405
|
+
setLayoutGateCard("checking");
|
|
406
|
+
setLayoutGateActive(true);
|
|
407
|
+
clearLayoutGateTimer();
|
|
408
|
+
|
|
409
|
+
const cycle = layoutGateCycle;
|
|
410
|
+
layoutGateTimer = setTimeout(() => {
|
|
411
|
+
if (cycle !== layoutGateCycle || !layoutGateVisible || ended) return;
|
|
412
|
+
forceRevealLayoutGate("timeout");
|
|
413
|
+
}, layoutGateMaxHoldMs);
|
|
414
|
+
layoutGateTimer?.unref?.();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function handleLayoutWarningsForGate(layoutWarnings) {
|
|
418
|
+
const warnings = normalizeLayoutWarningsPayload(layoutWarnings);
|
|
419
|
+
const hasErrors = warnings.some(isErrorLayoutWarning);
|
|
420
|
+
|
|
421
|
+
if (!layoutGateEnabled) return;
|
|
422
|
+
|
|
423
|
+
if (layoutGateManuallyBypassed) {
|
|
424
|
+
setLayoutIssueBanner(hasErrors);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!layoutGateArmed && !layoutGateVisible) return;
|
|
429
|
+
|
|
430
|
+
if (!hasErrors) {
|
|
431
|
+
revealLayoutGate();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
setLayoutGateCard("held");
|
|
436
|
+
setLayoutGateActive(true);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function initializeLayoutGate() {
|
|
440
|
+
if (!layoutGateEnabled) {
|
|
441
|
+
setLayoutGateActive(false);
|
|
442
|
+
setLayoutIssueBanner(false);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (layoutGateAction) layoutGateAction.onclick = () => forceRevealLayoutGate("manual");
|
|
447
|
+
startLayoutGateCycle();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function submitLayoutWarnings(layoutWarnings) {
|
|
451
|
+
const response = await fetch("/api/" + key + "/layout-warnings", {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: { "content-type": "application/json" },
|
|
454
|
+
body: JSON.stringify({ layout_warnings: normalizeLayoutWarningsPayload(layoutWarnings) }),
|
|
455
|
+
});
|
|
456
|
+
if (!response.ok) throw new Error("failed to submit layout warnings");
|
|
457
|
+
}
|
|
458
|
+
|
|
294
459
|
async function endSession() {
|
|
295
460
|
if (ended) return;
|
|
296
461
|
const response = await fetch("/api/" + key + "/end", { method: "POST" });
|
|
@@ -302,6 +467,8 @@ async function endSession() {
|
|
|
302
467
|
chatInput.disabled = true;
|
|
303
468
|
updateSendState();
|
|
304
469
|
if (presenceBanner) presenceBanner.hidden = true;
|
|
470
|
+
layoutGateManuallyBypassed = true;
|
|
471
|
+
revealLayoutGate();
|
|
305
472
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: false });
|
|
306
473
|
endedOverlay.hidden = false;
|
|
307
474
|
}
|
|
@@ -323,9 +490,13 @@ function copyDomSnapshot() {
|
|
|
323
490
|
}
|
|
324
491
|
|
|
325
492
|
function resetFrame() {
|
|
493
|
+
startLayoutGateCycle();
|
|
326
494
|
// The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
|
|
327
|
-
|
|
328
|
-
|
|
495
|
+
frame.src = artifactSrc || frame.src;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function loadFrame() {
|
|
499
|
+
if (artifactSrc) frame.src = artifactSrc;
|
|
329
500
|
}
|
|
330
501
|
|
|
331
502
|
function reloadArtifact() {
|
|
@@ -359,9 +530,7 @@ window.addEventListener("message", (event) => {
|
|
|
359
530
|
|
|
360
531
|
const msg = event.data || {};
|
|
361
532
|
if (msg.type === "lavish:queuePrompt") {
|
|
362
|
-
|
|
363
|
-
persistQueuedPrompts();
|
|
364
|
-
render();
|
|
533
|
+
enqueuePrompt(msg.prompt);
|
|
365
534
|
}
|
|
366
535
|
if (msg.type === "lavish:snapshot") {
|
|
367
536
|
const snapshotAction = snapshotRequests.shift() || "submit";
|
|
@@ -375,10 +544,16 @@ window.addEventListener("message", (event) => {
|
|
|
375
544
|
if (msg.type === "lavish:scroll") {
|
|
376
545
|
lastScroll = { x: Number(msg.x) || 0, y: Number(msg.y) || 0 };
|
|
377
546
|
}
|
|
547
|
+
if (msg.type === "lavish:layoutWarnings") {
|
|
548
|
+
handleLayoutWarningsForGate(msg.layout_warnings);
|
|
549
|
+
submitLayoutWarnings(msg.layout_warnings).catch(() => {});
|
|
550
|
+
}
|
|
378
551
|
if (msg.type === "lavish:sendQueuedPrompts") sendQueued();
|
|
379
552
|
if (msg.type === "lavish:endSession") endSession();
|
|
380
553
|
});
|
|
381
554
|
|
|
555
|
+
loadFrame();
|
|
556
|
+
|
|
382
557
|
annotationSwitch.onclick = () => {
|
|
383
558
|
annotation = !annotation;
|
|
384
559
|
annotationSwitch.setAttribute("aria-pressed", String(annotation));
|
|
@@ -418,6 +593,8 @@ frame.addEventListener("load", () => {
|
|
|
418
593
|
postToFrame({ type: "lavish:restoreScroll", x: lastScroll.x, y: lastScroll.y });
|
|
419
594
|
});
|
|
420
595
|
|
|
596
|
+
initializeLayoutGate();
|
|
597
|
+
|
|
421
598
|
const events = new EventSource("/events/" + key);
|
|
422
599
|
events.addEventListener("reload", () => resetFrame());
|
|
423
600
|
events.addEventListener("chrome-reload", () => reloadAfterServerRestart());
|
package/dist/chrome.css
CHANGED
|
@@ -434,6 +434,8 @@ body.lavish {
|
|
|
434
434
|
min-width: 0;
|
|
435
435
|
min-height: 0;
|
|
436
436
|
background: #fff;
|
|
437
|
+
position: relative;
|
|
438
|
+
overflow: hidden;
|
|
437
439
|
}
|
|
438
440
|
.panel {
|
|
439
441
|
width: var(--panel-w);
|
|
@@ -685,11 +687,37 @@ body.lavish {
|
|
|
685
687
|
line-height: 1.45;
|
|
686
688
|
overflow-wrap: anywhere;
|
|
687
689
|
}
|
|
690
|
+
.ended-action {
|
|
691
|
+
margin-top: var(--space-8);
|
|
692
|
+
}
|
|
693
|
+
.layout-issue-banner {
|
|
694
|
+
position: absolute;
|
|
695
|
+
top: var(--space-8);
|
|
696
|
+
left: var(--space-8);
|
|
697
|
+
right: var(--space-8);
|
|
698
|
+
z-index: 20;
|
|
699
|
+
border: 1px solid rgba(244, 201, 93, 0.35);
|
|
700
|
+
border-radius: var(--radius-lg);
|
|
701
|
+
background: rgba(37, 35, 15, 0.92);
|
|
702
|
+
color: var(--brass-400);
|
|
703
|
+
padding: var(--space-6) var(--space-8);
|
|
704
|
+
font-size: var(--text-sm);
|
|
705
|
+
line-height: var(--lh-sm);
|
|
706
|
+
box-shadow: var(--shadow-tooltip);
|
|
707
|
+
}
|
|
708
|
+
.layout-issue-banner[hidden] {
|
|
709
|
+
display: none;
|
|
710
|
+
}
|
|
688
711
|
iframe {
|
|
689
712
|
width: 100%;
|
|
690
713
|
height: 100%;
|
|
691
714
|
border: 0;
|
|
692
715
|
background: white;
|
|
716
|
+
opacity: 1;
|
|
717
|
+
transition: opacity var(--dur-fast) var(--ease);
|
|
718
|
+
}
|
|
719
|
+
body.layout-gate-active iframe#artifact {
|
|
720
|
+
opacity: 0;
|
|
693
721
|
}
|
|
694
722
|
@media (max-width: 860px) {
|
|
695
723
|
body {
|