lavish-axi 0.1.46 → 0.1.48

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
@@ -162,6 +162,8 @@ pnpm link
162
162
  - **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, feedback controls, and render-time layout checks.
163
163
  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.
164
164
  Run `lavish-axi design` for the single source of agent-facing design guidance and optional CDN or Mermaid snippets.
165
+ - **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`.
166
+ 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.
165
167
  - **Open-time layout gate** - The browser chrome masks an artifact only while the real in-iframe audit waits for fonts and final geometry.
166
168
  The first completed check always reveals the artifact, whatever it found; the gate never holds the review hostage waiting for a repair.
167
169
  The user can click **Show anyway**, and a bounded safety timeout fails open when no check has completed.
@@ -8,7 +8,6 @@ const queueStorageKey = "lavish-axi:queued:" + key;
8
8
  // Review-chrome state that must survive a browser refresh. Keyed per session so one review's
9
9
  // triage can never leak into another artifact's.
10
10
  const warningSelectionStorageKey = "lavish-axi:warning-selection:" + key;
11
- const warningAckStorageKey = "lavish-axi:warning-ack:" + key;
12
11
  const internalQueueKeyField = "_lavishQueueKey";
13
12
  const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
14
13
  const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
@@ -57,7 +56,6 @@ const layoutGateOverlay = /** @type {HTMLDivElement} */ (document.getElementById
57
56
  const layoutGateTitle = /** @type {HTMLDivElement} */ (document.getElementById("layoutGateTitle"));
58
57
  const layoutGateCopy = /** @type {HTMLParagraphElement} */ (document.getElementById("layoutGateCopy"));
59
58
  const layoutGateAction = /** @type {HTMLButtonElement} */ (document.getElementById("layoutGateAction"));
60
- const layoutIssueBanner = /** @type {HTMLDivElement} */ (document.getElementById("layoutIssueBanner"));
61
59
  const warningsWrap = /** @type {HTMLDivElement} */ (document.getElementById("warningsWrap"));
62
60
  const warningsButton = /** @type {HTMLButtonElement} */ (document.getElementById("warningsButton"));
63
61
  const warningsCount = /** @type {HTMLSpanElement} */ (document.getElementById("warningsCount"));
@@ -96,7 +94,6 @@ let layoutGateTimer;
96
94
  let layoutWarnings = Array.isArray(sessionData.initialLayoutWarnings) ? sessionData.initialLayoutWarnings : [];
97
95
  const selectedWarningIds = new Set(loadJsonState(warningSelectionStorageKey, []));
98
96
  let warningsDrawerOpen = false;
99
- let warningsAcknowledged = loadJsonState(warningAckStorageKey, false) === true;
100
97
  const snapshotRequests = [];
101
98
  let endAfterSubmit = false;
102
99
  let workingBubble = null;
@@ -482,17 +479,6 @@ function normalizeLayoutFindings(value) {
482
479
  : [];
483
480
  }
484
481
 
485
- function setLayoutIssueBanner(visible) {
486
- if (!layoutIssueBanner) return;
487
- layoutIssueBanner.hidden = !visible;
488
- }
489
-
490
- // The banner is a one-time "there is unresolved work" cue for the top-bar inbox. Once the user
491
- // has opened the drawer it stays out of the way; the badge remains the standing signal.
492
- function refreshLayoutIssueBanner() {
493
- setLayoutIssueBanner(!ended && !warningsAcknowledged && !layoutGateVisible && activeWarnings().length > 0);
494
- }
495
-
496
482
  function clearLayoutGateTimer() {
497
483
  if (layoutGateTimer) clearTimeout(layoutGateTimer);
498
484
  layoutGateTimer = undefined;
@@ -522,7 +508,6 @@ function revealLayoutGate() {
522
508
  clearLayoutGateTimer();
523
509
  layoutGateArmed = false;
524
510
  setLayoutGateActive(false);
525
- refreshLayoutIssueBanner();
526
511
  }
527
512
 
528
513
  function forceRevealLayoutGate(reason) {
@@ -536,7 +521,6 @@ function startLayoutGateCycle() {
536
521
 
537
522
  layoutGateCycle += 1;
538
523
  layoutGateArmed = true;
539
- setLayoutIssueBanner(false);
540
524
  setLayoutGateCard("checking");
541
525
  setLayoutGateActive(true);
542
526
  clearLayoutGateTimer();
@@ -553,21 +537,14 @@ function startLayoutGateCycle() {
553
537
  // pending an agent repair: findings are the user's to triage, so a completed pass always reveals
554
538
  // and hands the result to the passive inbox.
555
539
  function handleLayoutGatePass() {
556
- if (!layoutGateEnabled || layoutGateManuallyBypassed) {
557
- refreshLayoutIssueBanner();
558
- return;
559
- }
560
- if (!layoutGateArmed && !layoutGateVisible) {
561
- refreshLayoutIssueBanner();
562
- return;
563
- }
540
+ if (!layoutGateEnabled || layoutGateManuallyBypassed) return;
541
+ if (!layoutGateArmed && !layoutGateVisible) return;
564
542
  revealLayoutGate();
565
543
  }
566
544
 
567
545
  function initializeLayoutGate() {
568
546
  if (!layoutGateEnabled) {
569
547
  setLayoutGateActive(false);
570
- setLayoutIssueBanner(false);
571
548
  return;
572
549
  }
573
550
 
@@ -821,7 +798,6 @@ function renderWarnings() {
821
798
  for (const warning of active) warningsList.appendChild(createWarningRow(warning));
822
799
  }
823
800
  updateWarningSelectionState();
824
- refreshLayoutIssueBanner();
825
801
  }
826
802
 
827
803
  function updateWarningSelectionState() {
@@ -854,9 +830,6 @@ function setWarningsDrawerOpen(open) {
854
830
  warningsButton.setAttribute("aria-expanded", String(warningsDrawerOpen));
855
831
  if (warningsDrawerOpen) {
856
832
  closeMenus();
857
- warningsAcknowledged = true;
858
- saveJsonState(warningAckStorageKey, true);
859
- refreshLayoutIssueBanner();
860
833
  warningsSelectAll.focus();
861
834
  }
862
835
  }
package/dist/chrome.css CHANGED
@@ -880,7 +880,6 @@ body.lavish {
880
880
  min-width: 0;
881
881
  min-height: 0;
882
882
  background: #fff;
883
- position: relative;
884
883
  overflow: hidden;
885
884
  }
886
885
  .panel {
@@ -1194,24 +1193,6 @@ body.lavish {
1194
1193
  .ended-action {
1195
1194
  margin-top: var(--space-8);
1196
1195
  }
1197
- .layout-issue-banner {
1198
- position: absolute;
1199
- top: var(--space-8);
1200
- left: var(--space-8);
1201
- right: var(--space-8);
1202
- z-index: 20;
1203
- border: 1px solid rgba(244, 201, 93, 0.35);
1204
- border-radius: var(--radius-lg);
1205
- background: rgba(37, 35, 15, 0.92);
1206
- color: var(--brass-400);
1207
- padding: var(--space-6) var(--space-8);
1208
- font-size: var(--text-sm);
1209
- line-height: var(--lh-sm);
1210
- box-shadow: var(--shadow-tooltip);
1211
- }
1212
- .layout-issue-banner[hidden] {
1213
- display: none;
1214
- }
1215
1196
  iframe {
1216
1197
  width: 100%;
1217
1198
  height: 100%;
package/dist/cli.mjs CHANGED
@@ -447,7 +447,7 @@ function createDesignOutput() {
447
447
  playbooks: listPlaybooks()
448
448
  },
449
449
  design: {
450
- summary: "Use this Lavish CDN fallback only if (1) the user gave no design direction and (2) you already inspected the project the artifact is about and found no design system or style conventions to match. If you have not checked the subject project yet, check first. Lavish does not auto-inject any design system; artifacts stay portable HTML. " + DESIGN_PRIORITY_RULE + " Paste the CDN snippet below into your `<head>`.",
450
+ summary: "Use this Lavish CDN fallback only if (1) the user gave no design direction and (2) you already inspected the project the artifact is about and found no design system or style conventions to match. If you have not checked the subject project yet, check first. Lavish does not auto-inject any design system; artifacts stay portable HTML. Paint an explicit page background and readable text. " + DESIGN_PRIORITY_RULE + " Paste the CDN snippet below into your `<head>`.",
451
451
  cdn_snippet: DESIGN_CDN_SNIPPET,
452
452
  cdn_urls: DESIGN_CDN_URLS,
453
453
  versions: { tailwind: TAILWIND_BROWSER_VERSION, daisyui: DAISYUI_VERSION },
@@ -4450,6 +4450,48 @@ function linkFailureReason(error) {
4450
4450
  return message || "link creation failed";
4451
4451
  }
4452
4452
 
4453
+ // src/self-paint.js
4454
+ var ROOT_TAG_RE = /<(?:html|body)\b([^>]*)>/gi;
4455
+ var STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
4456
+ var CSS_RULE_RE = /([^{}]+)\{([^{}]*)\}/g;
4457
+ var ROOT_SELECTOR_TOKEN_RE = /(^|[\s,>~+])(html|body|:root|\*)(?![\w-])/i;
4458
+ var SELF_PAINT_WARNING = "This artifact never paints its own page surface: no background on html/body/:root, no bg-* class or data-theme on html/body, and no stylesheet that could set one. Lavish injects no design system, so text that assumes a dark or light host surface can render invisible. Set an explicit background and readable text.";
4459
+ function analyzeSelfPaint(html) {
4460
+ const source = typeof html === "string" ? html : "";
4461
+ if (/<link\b[^>]*\brel\s*=\s*["']?[^"'>]*stylesheet/i.test(source)) {
4462
+ return { painted: true, signal: "stylesheet-link" };
4463
+ }
4464
+ if (/<script\b[^>]*\bsrc\s*=\s*["']?[^"'>]*tailwind/i.test(source)) {
4465
+ return { painted: true, signal: "tailwind-runtime" };
4466
+ }
4467
+ if (/<meta\b[^>]*\bname\s*=\s*["']?color-scheme/i.test(source)) {
4468
+ return { painted: true, signal: "color-scheme" };
4469
+ }
4470
+ for (const [, attrs] of source.matchAll(ROOT_TAG_RE)) {
4471
+ if (/\bdata-theme\s*=/i.test(attrs)) return { painted: true, signal: "data-theme" };
4472
+ const className = attrValue(attrs, "class");
4473
+ if (className && /(^|[^\w-])bg-/i.test(className)) return { painted: true, signal: "background-class" };
4474
+ const style = attrValue(attrs, "style");
4475
+ if (style && /background/i.test(style)) return { painted: true, signal: "inline-background" };
4476
+ if (style && /color-scheme\s*:/i.test(style)) return { painted: true, signal: "color-scheme" };
4477
+ }
4478
+ for (const [, css] of source.matchAll(STYLE_BLOCK_RE)) {
4479
+ const stripped = css.replace(/\/\*[\s\S]*?\*\//g, "");
4480
+ if (/@import\b/i.test(stripped)) return { painted: true, signal: "css-import" };
4481
+ if (/color-scheme\s*:/i.test(stripped)) return { painted: true, signal: "color-scheme" };
4482
+ for (const [, selector, declarations] of stripped.matchAll(CSS_RULE_RE)) {
4483
+ if (!/background/i.test(declarations)) continue;
4484
+ if (ROOT_SELECTOR_TOKEN_RE.test(selector)) return { painted: true, signal: "root-background-rule" };
4485
+ }
4486
+ }
4487
+ return { painted: false, signal: null };
4488
+ }
4489
+ function attrValue(attrs, name) {
4490
+ const match = attrs.match(new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
4491
+ if (!match) return null;
4492
+ return match[1] ?? match[2] ?? match[3] ?? null;
4493
+ }
4494
+
4453
4495
  // src/server.js
4454
4496
  import crypto4 from "node:crypto";
4455
4497
  import { EventEmitter } from "node:events";
@@ -8430,7 +8472,7 @@ ${faviconTag}
8430
8472
  </head>
8431
8473
  <body class="${bodyClass}">
8432
8474
  <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>
8433
- <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>Layout issues detected. Open <strong>Layout issues</strong> in the top bar to review and queue fixes.</div></div><aside class="panel"><h2>Conversation</h2><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></div><div class="composer"><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" 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="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>
8475
+ <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><aside class="panel"><h2>Conversation</h2><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></div><div class="composer"><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" 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="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>
8434
8476
  <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>
8435
8477
  <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>
8436
8478
  <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>
@@ -8674,7 +8716,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
8674
8716
  ]);
8675
8717
  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.";
8676
8718
  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.";
8677
- var VERSION = "0.1.46";
8719
+ var VERSION = "0.1.48";
8678
8720
  function detectInvokingAgent(env = process.env) {
8679
8721
  return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
8680
8722
  }
@@ -8818,10 +8860,12 @@ function createPlaybookOutput(args) {
8818
8860
  }
8819
8861
  return { playbook };
8820
8862
  }
8821
- function createOpenOutput({ file, url, status, agent = "generic" }) {
8863
+ function createOpenOutput({ file, url, status, agent = "generic", selfPaintWarning = void 0 }) {
8864
+ const selfPaintPrefix = selfPaintWarning ? `First fix the unpainted page surface flagged in self_paint_warning and save - Lavish live-reloads the artifact automatically, so you do not need to re-run \`lavish-axi ${file}\`. ` : "";
8822
8865
  return {
8823
8866
  session: { file, url, status },
8824
- 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 or ends the session, and it stays silent the whole time - that is normal, never kill it. Layout issues the browser detects do not return this poll; they wait in the user's Layout issues inbox until the user queues them, then arrive as an ordinary tag "layout-warnings" prompt. Do not pass --timeout-ms during normal agent use. ${pollExecutionGuidance({ agent })} After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show 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\`.`
8867
+ ...selfPaintWarning ? { self_paint_warning: selfPaintWarning } : {},
8868
+ next_step: `${selfPaintPrefix}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback or ends the session, and it stays silent the whole time - that is normal, never kill it. Layout issues the browser detects do not return this poll; they wait in the user's Layout issues inbox until the user queues them, then arrive as an ordinary tag "layout-warnings" prompt. Do not pass --timeout-ms during normal agent use. ${pollExecutionGuidance({ agent })} After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show 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\`.`
8825
8869
  };
8826
8870
  }
8827
8871
  function createUserEndedOpenOutput({ file, url }) {
@@ -8837,6 +8881,7 @@ async function openCommand(args) {
8837
8881
  }
8838
8882
  await assertHtmlFile(file);
8839
8883
  const absolute = await canonicalFile(file);
8884
+ const selfPaintWarning = await selfPaintWarningForFile(absolute);
8840
8885
  const noGate = args.includes("--no-gate");
8841
8886
  const reopen = args.includes("--reopen");
8842
8887
  const baseUrl = await ensureServer({ forceRestart: shouldForceRestartForLocalBuild(process.argv[1] || "") });
@@ -8856,9 +8901,17 @@ async function openCommand(args) {
8856
8901
  file: absolute,
8857
8902
  url: response.url,
8858
8903
  status: response.status || "opened",
8859
- agent: detectInvokingAgent(process.env)
8904
+ agent: detectInvokingAgent(process.env),
8905
+ selfPaintWarning
8860
8906
  });
8861
8907
  }
8908
+ async function selfPaintWarningForFile(absolute) {
8909
+ try {
8910
+ return analyzeSelfPaint(await readFile5(absolute, "utf8")).painted ? void 0 : SELF_PAINT_WARNING;
8911
+ } catch {
8912
+ return void 0;
8913
+ }
8914
+ }
8862
8915
  function shouldOpenBrowser(args, env) {
8863
8916
  return !args.includes("--no-open") && env.LAVISH_AXI_NO_OPEN !== "1";
8864
8917
  }
@@ -9020,9 +9073,15 @@ async function exportCommand(args) {
9020
9073
  resolveAbsolute: resolveDesignAssetPath
9021
9074
  });
9022
9075
  await writeFile3(output, html);
9023
- return createExportOutput({ source: absolute, output, html, warnings });
9076
+ return createExportOutput({
9077
+ source: absolute,
9078
+ output,
9079
+ html,
9080
+ warnings,
9081
+ selfPaintWarning: analyzeSelfPaint(source).painted ? void 0 : SELF_PAINT_WARNING
9082
+ });
9024
9083
  }
9025
- function createExportOutput({ source, output, html, warnings }) {
9084
+ function createExportOutput({ source, output, html, warnings, selfPaintWarning = void 0 }) {
9026
9085
  const allWarnings = Array.isArray(warnings) ? warnings : [];
9027
9086
  const { unresolved, notices } = splitExportWarnings(allWarnings);
9028
9087
  const result = {
@@ -9044,6 +9103,10 @@ function createExportOutput({ source, output, html, warnings }) {
9044
9103
  } else {
9045
9104
  result.next_step = `Wrote ${output}. Open it directly or host it anywhere - it needs no Lavish server. Local assets are inlined; remote CDN/font references are left as links, so it needs network to render those.`;
9046
9105
  }
9106
+ if (selfPaintWarning) {
9107
+ result.self_paint_warning = selfPaintWarning;
9108
+ result.next_step = `Fix the unpainted page surface flagged in self_paint_warning and re-run the export before sharing the file - an exported page renders over whatever surface hosts it. ${result.next_step}`;
9109
+ }
9047
9110
  return result;
9048
9111
  }
9049
9112
  function assetWarningSummaries(warnings) {
@@ -9066,9 +9129,15 @@ async function shareCommand(args) {
9066
9129
  resolveAbsolute: resolveDesignAssetPath
9067
9130
  });
9068
9131
  const site = await publishToHtmlApp(html, { password, token });
9069
- return createShareOutput({ source: absolute, site, warnings, passwordProtected: Boolean(password) });
9132
+ return createShareOutput({
9133
+ source: absolute,
9134
+ site,
9135
+ warnings,
9136
+ passwordProtected: Boolean(password),
9137
+ selfPaintWarning: analyzeSelfPaint(source).painted ? void 0 : SELF_PAINT_WARNING
9138
+ });
9070
9139
  }
9071
- function createShareOutput({ source, site, warnings, passwordProtected = false }) {
9140
+ function createShareOutput({ source, site, warnings, passwordProtected = false, selfPaintWarning = void 0 }) {
9072
9141
  const allWarnings = Array.isArray(warnings) ? warnings : [];
9073
9142
  const { unresolved, notices } = splitExportWarnings(allWarnings);
9074
9143
  const isPasswordProtected = Boolean(passwordProtected);
@@ -9099,6 +9168,10 @@ function createShareOutput({ source, site, warnings, passwordProtected = false }
9099
9168
  } else {
9100
9169
  result.next_step = `Published a PUBLIC page that anyone with the link can view: ${site.url} - share this URL with the user. ${noticeNote ? `${noticeNote} ` : ""}The update_key is a secret shown only once; keep it to update or delete the page later (there is no recovery). ` + hostNote;
9101
9170
  }
9171
+ if (selfPaintWarning) {
9172
+ result.self_paint_warning = selfPaintWarning;
9173
+ result.next_step = `Fix the unpainted page surface flagged in self_paint_warning, then re-run the share command and share only its replacement URL - the hosted page renders over ht-ml.app's own surface. ${result.next_step}`;
9174
+ }
9102
9175
  return result;
9103
9176
  }
9104
9177
  async function stopCommand(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
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.46",
4
+ "version": "0.1.48",
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",
@@ -32,6 +32,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac
32
32
 
33
33
  1. Create the HTML artifact (default location `.lavish/<name>.html` in the working directory).
34
34
  2. Run `npx -y lavish-axi <html-file>` to open or resume a review session in the browser.
35
+ If the output carries a `self_paint_warning`, fix the unpainted page surface and save before polling - Lavish live-reloads the artifact.
35
36
  3. Run `npx -y lavish-axi poll <html-file>` to long-poll for the user's annotations and queued prompts.
36
37
  On the first poll, prefer `--agent-reply "<one-line summary of what you built and what to review first>"` so the conversation panel opens with context.
37
38
  Browser-detected layout issues are filed passively in the user's Layout issues inbox and arrive as an ordinary `layout-warnings` prompt only when the user selects and queues them. Never edit an issue the user has not queued. The only response that arrives without user action is `artifact_failures`, when the review surface itself is unusable.