lavish-axi 0.1.58 → 0.1.59

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 CHANGED
@@ -166,9 +166,9 @@ pnpm link
166
166
  - **Self-paint warning** - `lavish-axi <html-file>`, `export`, and `share` run a render-free check for artifacts missing an explicit page background and return a one-line `self_paint_warning`.
167
167
  The check fails open - any stylesheet link, `@import`, Tailwind runtime script, `color-scheme`, or `html`/`body`/`:root` background signal suppresses it - and it never blocks the open.
168
168
  - **Open-time layout gate** - The browser chrome masks an artifact only while the real in-iframe audit waits for fonts and final geometry.
169
- The first completed check reveals the artifact, whatever it found, unless the mask is naming one of the failures below; the gate never holds the review hostage waiting for a repair.
170
- The user can click **Show anyway**, and a bounded safety timeout fails open when no check has completed.
171
- If the review cannot load at all - the chrome's own script never runs, or the server does not answer the artifact's load request after several retries - the mask is replaced by a message naming the problem and a **Check and reload** button, instead of holding the artifact behind a check that will never complete.
169
+ The first completed client-side check reveals the artifact, whatever it found and even if reporting that check to the server fails; the gate never holds the review hostage waiting for a repair or a network round-trip.
170
+ The user can click **Show anyway**, and a bounded safety timeout fails open from every gate state.
171
+ If the review cannot load at all - the chrome's own script never runs, or the server does not answer the artifact's load request after several retries - the mask names the problem and offers **Check and reload** without removing the independent **Show anyway** escape.
172
172
  A review already loaded in another browser tab is named the same way, with a **Take over here** button that moves it into the current tab, because Lavish loads an artifact in one tab at a time.
173
173
  - **Layout issues inbox** - Detection is passive. After fonts and finite animations settle, the injected SDK confirms severe failures from direct rendered evidence such as materially escaped meaningful content or required controls, clipped text fragments, viewport reachability, or near-total semantic occlusion.
174
174
  Explicit ellipsis and line clamp, standard visually hidden accessibility text, intentional scrollers or masks, parent overhang, generic element scroll geometry, decorative overlap, and uncertain motion do not produce findings by themselves.
@@ -151,6 +151,8 @@ const layoutGateOverlay = /** @type {HTMLDivElement} */ (document.getElementById
151
151
  const layoutGateTitle = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateTitle"));
152
152
  const layoutGateCopy = /** @type {HTMLParagraphElement} */ (document.getElementById("layoutGateCopy"));
153
153
  const layoutGateAction = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateAction"));
154
+ const layoutGateBypass = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateBypass"));
155
+ const layoutGateEscape = /** @type {any} */ (window).__lavishLayoutGateEscape;
154
156
  const warningsWrap = /** @type {HTMLDivElement} */ (document.getElementById("warningsWrap"));
155
157
  const warningsButton = /** @type {HTMLButtonElement} */ (document.getElementById("warningsButton"));
156
158
  const warningsCount = /** @type {HTMLSpanElement} */ (document.getElementById("warningsCount"));
@@ -185,7 +187,6 @@ let outdatedReloadInFlight = false;
185
187
  let unrestorableDraftMiss = null;
186
188
  let retiredDrafts = loadRetiredDrafts();
187
189
  let layoutGateVisible = false;
188
- let layoutGateArmed = false;
189
190
  let layoutGateManuallyBypassed = !layoutGateEnabled;
190
191
  let layoutGateFailureActive = false;
191
192
  // A failure only the user can retire. The artifact-load card clears itself once a load succeeds;
@@ -1261,6 +1262,7 @@ function normalizeLayoutFindings(value) {
1261
1262
  }
1262
1263
 
1263
1264
  function clearLayoutGateTimer() {
1265
+ layoutGateEscape?.cancel?.();
1264
1266
  if (layoutGateTimer) clearTimeout(layoutGateTimer);
1265
1267
  layoutGateTimer = undefined;
1266
1268
  }
@@ -1289,7 +1291,8 @@ function setLayoutGateActive(active) {
1289
1291
  // its own: the artifact never loaded and retrying stopped helping. The overlay is reused because
1290
1292
  // it already covers the empty artifact area; without this the user is left looking at either a
1291
1293
  // spinner that never resolves or a blank frame, with nothing explaining it and nothing to click.
1292
- // Bumping the cycle retires any pending reveal timer so a stale one cannot hide this card.
1294
+ // Bumping the cycle invalidates the previous timer; setLayoutGateFailure immediately replaces it
1295
+ // with a fresh hold timer so the card cannot strand the visual gate.
1293
1296
  function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { sticky = false } = {}) {
1294
1297
  if (ended) return;
1295
1298
  // A sticky card is the user's to retire, and that has to hold against being overwritten as
@@ -1299,8 +1302,9 @@ function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { s
1299
1302
  layoutGateFailureActive = true;
1300
1303
  layoutGateFailureSticky = sticky;
1301
1304
  layoutGateCycle += 1;
1302
- clearLayoutGateTimer();
1303
- layoutGateArmed = false;
1305
+ // Failure copy must not disable the visual gate's own recovery paths. Keep a fresh hold timer
1306
+ // over the card so a server replacement or any other failure cannot strand the artifact behind
1307
+ // a sticky message forever.
1304
1308
  if (layoutGateTitle) layoutGateTitle.textContent = title;
1305
1309
  if (layoutGateCopy) layoutGateCopy.textContent = copy;
1306
1310
  if (layoutGateAction) {
@@ -1308,7 +1312,12 @@ function setLayoutGateFailure(title, copy, actionLabel = "Reload", onAction, { s
1308
1312
  layoutGateAction.textContent = actionLabel;
1309
1313
  layoutGateAction.onclick = onAction || (() => location.reload());
1310
1314
  }
1315
+ if (layoutGateBypass) {
1316
+ layoutGateBypass.hidden = false;
1317
+ layoutGateBypass.onclick = () => forceRevealLayoutGate("manual");
1318
+ }
1311
1319
  setLayoutGateActive(true);
1320
+ armLayoutGateTimer();
1312
1321
  }
1313
1322
 
1314
1323
  // Every failure card in this feature is raised in a state where the server may not be listening,
@@ -1362,35 +1371,31 @@ function clearLayoutGateFailure() {
1362
1371
  layoutGateAction.textContent = "Show anyway";
1363
1372
  layoutGateAction.onclick = () => forceRevealLayoutGate("manual");
1364
1373
  }
1374
+ if (layoutGateBypass) layoutGateBypass.hidden = true;
1365
1375
  revealLayoutGate();
1366
1376
  }
1367
1377
 
1368
1378
  function revealLayoutGate() {
1369
- if (layoutGateFailureSticky) return;
1370
1379
  clearLayoutGateTimer();
1371
- layoutGateArmed = false;
1380
+ layoutGateEscape?.reveal?.();
1372
1381
  setLayoutGateActive(false);
1373
1382
  }
1374
1383
 
1375
1384
  function forceRevealLayoutGate(reason) {
1376
- if (!layoutGateEnabled || ended) return;
1377
- if (reason === "manual") layoutGateManuallyBypassed = true;
1385
+ if (ended) return;
1386
+ if (reason === "manual") {
1387
+ layoutGateManuallyBypassed = true;
1388
+ layoutGateEscape?.manualReveal?.();
1389
+ }
1378
1390
  revealLayoutGate();
1379
1391
  }
1380
1392
 
1381
- function startLayoutGateCycle() {
1382
- clearLayoutGateFailure();
1383
- // A sticky failure owns the overlay until the user acts on it, so a later load must not repaint
1384
- // the checking card over the message it left there.
1385
- if (layoutGateFailureSticky) return;
1386
- if (!layoutGateEnabled || layoutGateManuallyBypassed || ended) return;
1387
-
1388
- layoutGateCycle += 1;
1389
- layoutGateArmed = true;
1390
- setLayoutGateCard("checking");
1391
- setLayoutGateActive(true);
1393
+ function armLayoutGateTimer() {
1392
1394
  clearLayoutGateTimer();
1393
-
1395
+ if (layoutGateEscape?.arm) {
1396
+ layoutGateEscape.arm(layoutGateMaxHoldMs, () => forceRevealLayoutGate("timeout"));
1397
+ return;
1398
+ }
1394
1399
  const cycle = layoutGateCycle;
1395
1400
  layoutGateTimer = setTimeout(() => {
1396
1401
  if (cycle !== layoutGateCycle || !layoutGateVisible || ended) return;
@@ -1399,22 +1404,35 @@ function startLayoutGateCycle() {
1399
1404
  layoutGateTimer?.unref?.();
1400
1405
  }
1401
1406
 
1407
+ function startLayoutGateCycle() {
1408
+ clearLayoutGateFailure();
1409
+ if (!layoutGateEnabled || layoutGateManuallyBypassed || ended) return;
1410
+
1411
+ layoutGateCycle += 1;
1412
+ setLayoutGateActive(true);
1413
+ // A sticky failure owns the card copy, but never the reveal. Do not repaint it as a checking
1414
+ // card, and do arm a fresh timer for reloads that happen while the sticky card is present.
1415
+ if (!layoutGateFailureSticky) setLayoutGateCard("checking");
1416
+ armLayoutGateTimer();
1417
+ }
1418
+
1402
1419
  // The gate only waits for fonts and final geometry now. It never holds the artifact hostage
1403
1420
  // pending an agent repair: findings are the user's to triage, so a completed pass always reveals
1404
1421
  // and hands the result to the passive inbox.
1405
1422
  function handleLayoutGatePass() {
1406
- if (!layoutGateEnabled || layoutGateManuallyBypassed) return;
1407
- if (!layoutGateArmed && !layoutGateVisible) return;
1423
+ if (ended || !layoutGateVisible) return;
1408
1424
  revealLayoutGate();
1409
1425
  }
1410
1426
 
1411
1427
  function initializeLayoutGate() {
1428
+ if (layoutGateEscape?.isManuallyBypassed?.()) layoutGateManuallyBypassed = true;
1412
1429
  if (!layoutGateEnabled) {
1413
1430
  setLayoutGateActive(false);
1414
1431
  return;
1415
1432
  }
1416
1433
 
1417
1434
  if (layoutGateAction) layoutGateAction.onclick = () => forceRevealLayoutGate("manual");
1435
+ if (layoutGateBypass) layoutGateBypass.onclick = () => forceRevealLayoutGate("manual");
1418
1436
  startLayoutGateCycle();
1419
1437
  }
1420
1438
 
@@ -1801,6 +1819,7 @@ function markSessionEnded() {
1801
1819
  layoutGateManuallyBypassed = true;
1802
1820
  layoutGateFailureSticky = false;
1803
1821
  revealLayoutGate();
1822
+ layoutGateEscape?.end?.();
1804
1823
  postToFrame({ type: "lavish:setAnnotationMode", enabled: false });
1805
1824
  endedOverlay.hidden = false;
1806
1825
  }
@@ -2737,14 +2756,24 @@ window.addEventListener("message", (event) => {
2737
2756
 
2738
2757
  const msg = event.data || {};
2739
2758
  const messageToken = String(msg.artifact_load_token || "");
2740
- if (messageToken !== artifactLoadToken) return;
2759
+ if (messageToken !== artifactLoadToken) {
2760
+ // A pass can be stamped by the load that just lost a token race. Ask the current artifact
2761
+ // document to run the audit again instead of consuming the only pass for this cycle.
2762
+ if (msg.type === "lavish:layoutDiagnostics") postToFrame({ type: "lavish:requestLayoutDiagnostics" });
2763
+ return;
2764
+ }
2741
2765
  const messageSequence = ++artifactMessageSequence;
2742
2766
  artifactSpokeToken = messageToken;
2743
2767
  clearTimeout(artifactSilenceTimer);
2744
2768
  if (msg.type === "lavish:layoutDiagnostics") {
2745
2769
  const diagnosticSequence = ++layoutDiagnosticSequence;
2770
+ const complete = msg.complete !== false;
2771
+ // The gate is visual, so the client-side settled pass is the release signal. Reporting the
2772
+ // pass is deliberately fire-and-forget: a server restart or a diagnostics 4xx/5xx must not
2773
+ // hold a rendered artifact hostage to a network round-trip.
2774
+ if (complete) handleLayoutGatePass();
2746
2775
  submitLayoutDiagnostics({
2747
- complete: msg.complete !== false,
2776
+ complete,
2748
2777
  targetPresenceComplete: msg.target_presence_complete === true,
2749
2778
  artifactRevision: msg.artifact_revision,
2750
2779
  artifactLoadToken: msg.artifact_load_token,
@@ -2759,9 +2788,14 @@ window.addEventListener("message", (event) => {
2759
2788
  if (messageSequence === artifactMessageSequence) armArtifactAvailabilityProbe(messageToken);
2760
2789
  return;
2761
2790
  }
2762
- if (msg.complete !== false) handleLayoutGatePass();
2763
2791
  })
2764
- .catch(() => {});
2792
+ .catch(() => {
2793
+ // A failed report is still a completed client-side pass. Keep this fallback explicit so a
2794
+ // future change cannot accidentally make the network request the gate's release path.
2795
+ if (complete && messageToken === artifactLoadToken && diagnosticSequence === layoutDiagnosticSequence) {
2796
+ handleLayoutGatePass();
2797
+ }
2798
+ });
2765
2799
  return;
2766
2800
  }
2767
2801
  // The artifact spoke, so it rendered and ran its SDK - there is nothing fatal to probe for.
@@ -3148,9 +3182,9 @@ setAgentPresence("waiting");
3148
3182
  // to wait for - start read-only instead of looking live until a Send gets silently refused.
3149
3183
  if (sessionData.initialEnded) markSessionEnded();
3150
3184
 
3151
- // Reaching this line is the only proof that this file parsed and ran to completion. The page it
3152
- // bootstraps ships with the layout-gate overlay already covering the artifact, and only this
3153
- // script ever takes it down - so the inline failsafe in the page holds it up until here.
3185
+ // Reaching this line is the only proof that this file parsed and ran to completion. The inline
3186
+ // bootstrap already owns the gate's bounded escape if this script fails; retire only its separate
3187
+ // boot-failure timer now that the full client has taken over.
3154
3188
  const chromeBootWindow = /** @type {Record<string, any>} */ (/** @type {unknown} */ (window));
3155
3189
  chromeBootWindow.__lavishChromeReady = true;
3156
3190
  chromeBootWindow.__lavishCancelChromeBootFailsafe?.();
package/dist/chrome.css CHANGED
@@ -1328,6 +1328,9 @@ body.lavish {
1328
1328
  .ended-action {
1329
1329
  margin-top: var(--space-8);
1330
1330
  }
1331
+ .layout-gate-bypass {
1332
+ margin-left: var(--space-4);
1333
+ }
1331
1334
  iframe {
1332
1335
  width: 100%;
1333
1336
  height: 100%;
package/dist/cli.mjs CHANGED
@@ -5373,6 +5373,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
5373
5373
  const target = whiteboardEntryByIndex(msg.diagramIndex);
5374
5374
  if (target) target.iframe.src = whiteboardFrameSrc(target);
5375
5375
  }
5376
+ if (msg.type === "lavish:requestLayoutDiagnostics") scheduleLayoutAudit(true);
5376
5377
  });
5377
5378
  function enhanceMermaid() {
5378
5379
  for (const svg of findMermaidSvgs()) {
@@ -5546,6 +5547,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
5546
5547
  let layoutAuditTimer = 0;
5547
5548
  let layoutAuditRun = 0;
5548
5549
  let lastLayoutAuditSignature = null;
5550
+ let layoutAuditPublishRequested = false;
5549
5551
  let layoutAuditPassSequence = 0;
5550
5552
  function toPixelNumber(value) {
5551
5553
  const parsed = Number.parseFloat(String(value || "0"));
@@ -6143,7 +6145,12 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
6143
6145
  new Promise((resolve) => window.setTimeout(resolve, layoutAuditAnimationMaxWaitMs))
6144
6146
  ]);
6145
6147
  if (!settled) {
6146
- for (const animation of finite) animation.finished.then(scheduleLayoutAudit, scheduleLayoutAudit);
6148
+ for (const animation of finite) {
6149
+ animation.finished.then(
6150
+ () => scheduleLayoutAudit(),
6151
+ () => scheduleLayoutAudit()
6152
+ );
6153
+ }
6147
6154
  }
6148
6155
  return settled;
6149
6156
  }
@@ -6151,7 +6158,8 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
6151
6158
  const severe = findings.filter((finding) => finding?.severity === "error");
6152
6159
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
6153
6160
  const signature = JSON.stringify({ complete, targetPresenceComplete, viewportWidth, severe });
6154
- if (signature === lastLayoutAuditSignature) return;
6161
+ if (!layoutAuditPublishRequested && signature === lastLayoutAuditSignature) return;
6162
+ layoutAuditPublishRequested = false;
6155
6163
  lastLayoutAuditSignature = signature;
6156
6164
  postArtifactMessage("lavish:layoutDiagnostics", {
6157
6165
  complete,
@@ -6183,7 +6191,8 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
6183
6191
  targetPresenceComplete
6184
6192
  );
6185
6193
  }
6186
- function scheduleLayoutAudit() {
6194
+ function scheduleLayoutAudit(publishRequested = false) {
6195
+ if (publishRequested) layoutAuditPublishRequested = true;
6187
6196
  if (layoutAuditTimer) window.clearTimeout(layoutAuditTimer);
6188
6197
  const runId = ++layoutAuditRun;
6189
6198
  layoutAuditTimer = window.setTimeout(() => {
@@ -6194,10 +6203,10 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
6194
6203
  }
6195
6204
  function startLayoutAudit() {
6196
6205
  scheduleLayoutAudit();
6197
- window.addEventListener("load", scheduleLayoutAudit, { once: true });
6198
- window.addEventListener("resize", scheduleLayoutAudit, { passive: true });
6199
- window.addEventListener("animationend", scheduleLayoutAudit, { passive: true });
6200
- window.addEventListener("transitionend", scheduleLayoutAudit, { passive: true });
6206
+ window.addEventListener("load", () => scheduleLayoutAudit(), { once: true });
6207
+ window.addEventListener("resize", () => scheduleLayoutAudit(), { passive: true });
6208
+ window.addEventListener("animationend", () => scheduleLayoutAudit(), { passive: true });
6209
+ window.addEventListener("transitionend", () => scheduleLayoutAudit(), { passive: true });
6201
6210
  }
6202
6211
  function reportLocalAssetFailure(event) {
6203
6212
  const el = event.target;
@@ -10022,23 +10031,36 @@ function extractArtifactHead(html) {
10022
10031
  return { faviconTag, title };
10023
10032
  }
10024
10033
  var CHROME_BOOT_FAILSAFE_MS = 15e3;
10034
+ var CHROME_LAYOUT_GATE_MAX_HOLD_MS = 12e3;
10025
10035
  var CHROME_BOOT_FAILSAFE_PROBE_TIMEOUT_MS = 4e3;
10026
10036
  var CHROME_BOOT_FAILSAFE_JS = `(function(){
10027
10037
  var t=setTimeout(fail,${CHROME_BOOT_FAILSAFE_MS});
10028
- var o,h,c,a;
10038
+ var o=document.getElementById("layoutGateOverlay"),h,c,a,b,gt=0,manual=false,ended=false;
10039
+ try{ended=JSON.parse(document.getElementById("lavish-session").textContent).initialEnded===true;}catch(e){}
10040
+ function cancelGate(){if(gt)clearTimeout(gt);gt=0;}
10041
+ function reveal(){cancelGate();if(o)o.hidden=true;if(document.body)document.body.classList.remove("layout-gate-active");}
10042
+ function manualReveal(){if(ended)return false;manual=true;reveal();return true;}
10043
+ function armGate(ms,onTimeout){cancelGate();if(!ended)gt=setTimeout(function(){if(ended)return;if(onTimeout)onTimeout();else reveal();},ms);}
10044
+ function showBypass(){b=document.getElementById("layoutGateBypass");if(b){b.hidden=false;b.onclick=manualReveal;}}
10045
+ window.__lavishLayoutGateEscape={arm:armGate,cancel:cancelGate,reveal:reveal,manualReveal:manualReveal,showBypass:showBypass,end:function(){ended=true;cancelGate();},isEnded:function(){return ended;},isManuallyBypassed:function(){return manual;}};
10046
+ a=document.getElementById("layoutGateAction");
10047
+ if(ended){reveal();b=document.getElementById("endedOverlay");if(b)b.hidden=false;}
10048
+ if(a&&!ended)a.onclick=manualReveal;
10049
+ if(o&&!o.hidden&&!ended)armGate(${CHROME_LAYOUT_GATE_MAX_HOLD_MS});
10029
10050
  window.__lavishCancelChromeBootFailsafe=function(){clearTimeout(t);};
10030
10051
  window.__lavishChromeBootFailed=function(){clearTimeout(t);fail();};
10031
10052
  function fail(){
10032
- if(window.__lavishChromeReady)return;
10033
- o=document.getElementById("layoutGateOverlay");
10053
+ if(window.__lavishChromeReady||ended)return;
10034
10054
  h=document.getElementById("layoutGateTitle");
10035
10055
  c=document.getElementById("layoutGateCopy");
10036
10056
  a=document.getElementById("layoutGateAction");
10037
10057
  if(h)h.textContent="Lavish could not finish loading.";
10038
10058
  if(c)c.textContent="The Lavish editor script did not load. The server usually restarted while this page was opening. Check and reload to reconnect.";
10039
10059
  if(a){a.textContent="Check and reload";a.disabled=false;a.onclick=check;}
10060
+ showBypass();
10040
10061
  if(o)o.hidden=false;
10041
10062
  if(document.body)document.body.classList.add("layout-gate-active");
10063
+ armGate(${CHROME_LAYOUT_GATE_MAX_HOLD_MS});
10042
10064
  }
10043
10065
  function check(){
10044
10066
  if(a)a.disabled=true;
@@ -10105,7 +10127,7 @@ ${faviconTag}
10105
10127
  <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><div class="warnings-wrap" id="warningsWrap" hidden><button class="warnings-button" id="warningsButton" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="warningsDrawer">${chromeIcons.warning}<span class="warnings-count" id="warningsCount">0</span></button><div class="menu warnings-drawer" id="warningsDrawer" role="dialog" aria-labelledby="warningsTitle" aria-describedby="warningsSummary" hidden><div class="warnings-head"><h2 class="warnings-title" id="warningsTitle">Layout issues</h2><p class="warnings-summary" id="warningsSummary"></p></div><div class="warnings-toolbar"><label class="warnings-selectall"><input type="checkbox" id="warningsSelectAll"><span>Select all</span></label><span class="warnings-selected" id="warningsSelected" role="status" aria-live="polite"></span></div><div class="warnings-list" id="warningsList"></div><div class="warnings-foot"><p class="warnings-note">Queueing sends a repair request with your next feedback. An issue is marked resolved only after a newer artifact load and a complete check at the same viewport no longer finds it.</p><button class="button" id="warningsQueueButton" type="button" disabled>Queue selected fixes</button></div></div></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>
10106
10128
  <div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe></div><div class="panel-scrim" id="panelScrim"></div><aside class="panel" id="panel"><div class="panel-head" id="panelHead"><span class="panel-handle" aria-hidden="true"></span><div class="panel-head-row"><h2>Conversation</h2><span class="panel-summary" id="panelSummary" role="status" aria-live="polite"></span><button class="panel-toggle" id="panelToggle" type="button" aria-expanded="false" aria-controls="panel" aria-label="Show conversation">${chromeIcons.chevronUp}</button></div></div><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></div><div class="composer" id="chatComposer"><div class="presence-banner handoff-banner" id="handoffBanner" hidden><span>This review is open in another Lavish tab.</span><button class="handoff-takeover" id="handoffTakeover" type="button">Take over here</button></div><div class="presence-banner handoff-banner" id="outdatedBanner" hidden><span id="outdatedText">The Lavish server this page was connected to is no longer running. Reloading will work once it is running again.</span><span class="outdated-actions"><button class="handoff-takeover" id="outdatedReload" type="button">Check and reload</button><button class="handoff-takeover" id="outdatedDismiss" type="button">Dismiss</button></span></div><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><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="chat-attachments" id="chatAttachments"></div><div class="chat-attachment-toolbar"><button class="chat-attach" id="chatAttach" type="button">Attach images</button><input id="chatAttachInput" type="file" accept="${escapeHtml(acceptedMime.join(","))}" multiple hidden><span class="chat-attachment-notice" id="chatAttachmentNotice" role="status"></span></div><div class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</div><div class="actions" id="sendActions"><button class="button button-danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send &amp; End</span></button><button class="button" id="send">Send to Agent</button></div></div></aside></div>
10107
10129
  <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>
10108
- <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>
10130
+ <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><button class="button ended-action layout-gate-bypass" id="layoutGateBypass" type="button" hidden>Show anyway</button></div></div>
10109
10131
  <div class="ended-overlay" id="endedOverlay" hidden><div class="ended-card"><div class="ended-title">Session ended.<br>Return to your agent to continue.</div><p class="ended-copy">${escapeHtml(session.file)}</p></div></div>
10110
10132
  <div class="whiteboard-overlay" id="whiteboardOverlay" hidden><div class="whiteboard-shell"><div class="whiteboard-error" id="whiteboardError" hidden></div><button class="whiteboard-close" id="whiteboardClose" type="button" aria-label="Close whiteboard"><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><iframe id="whiteboardFrame" title="Excalidraw whiteboard" sandbox="allow-scripts allow-popups"></iframe></div></div>
10111
10133
  <script id="lavish-session" type="application/json">${sessionJson}</script>
@@ -10372,7 +10394,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
10372
10394
  ]);
10373
10395
  var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback is still delivered once. After that response, polling stops, and the agent must not reopen the session uninvited.";
10374
10396
  var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
10375
- var VERSION = "0.1.58";
10397
+ var VERSION = "0.1.59";
10376
10398
  function detectInvokingAgent(env = process.env) {
10377
10399
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
10378
10400
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "packageManager": "pnpm@11.1.1",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "type": "module",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "lavish-axi",
4
- "version": "0.1.58",
4
+ "version": "0.1.59",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "author": {
7
7
  "name": "Kun Chen",