lavish-axi 0.1.33 → 0.1.34

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
@@ -125,6 +125,7 @@ pnpm link
125
125
  ┌────────────────────────┐
126
126
  │ lavish-axi poll waits │
127
127
  │ and returns prompts │
128
+ │ or layout warnings │
128
129
  └────────────────────────┘
129
130
  ```
130
131
 
@@ -138,9 +139,10 @@ pnpm link
138
139
  - **Open-time layout gate** - The browser chrome masks each artifact until the real in-iframe layout audit reports no error-severity findings.
139
140
  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
141
  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.
142
+ - **Layout warnings** - After fonts load and layout settles, the injected SDK audits the real browser render for page horizontal overflow, element overflow, clipped or visibly spilling text, and overlapping text.
143
+ Intentional horizontal scrollers using `overflow-x: auto` or `scroll` are excluded from horizontal checks, and `overflow-y: auto` or `scroll` is treated as intentional for vertical overflow.
144
+ Current findings are returned from `lavish-axi poll` as `layout_warnings` with `selector`, `kind`, `overflowPx`, `viewportWidth`, `severity`, and `persistent`.
145
+ Fresh error-severity findings should be fixed and rechecked before asking the human to review; repeated or warning-only findings can be surfaced to the human with a note when the cause is not obvious.
144
146
  - **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.
145
147
  - **Export and sharing** - `lavish-axi export` writes `<name>.export.html` by inlining local assets only, stripping the annotation SDK, and leaving remote CDN/font references as links that still need network access.
146
148
  `lavish-axi share` publishes the same local-inlined HTML to [ht-ml.app](https://ht-ml.app), a third-party hosting service not part of Lavish.
package/dist/cli.mjs CHANGED
@@ -4277,6 +4277,57 @@ function isNativeInteractiveControl(el) {
4277
4277
  "button,input,select,textarea,option,optgroup,label,summary,[contenteditable]:not([contenteditable='false'])"
4278
4278
  ));
4279
4279
  }
4280
+ function fragmentsSignificantlyOverlap(fragmentsA, fragmentsB, { minAreaRatio = 0.25, minAreaPx = 24 } = {}) {
4281
+ function rectAreaOf(rect) {
4282
+ return Math.max(0, rect.width) * Math.max(0, rect.height);
4283
+ }
4284
+ function intersectionAreaOf(a, b) {
4285
+ const width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
4286
+ const height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
4287
+ return width * height;
4288
+ }
4289
+ for (const a of fragmentsA) {
4290
+ const threshold = Math.min(rectAreaOf(a) * minAreaRatio, minAreaPx);
4291
+ for (const b of fragmentsB) {
4292
+ if (intersectionAreaOf(a, b) >= threshold) return true;
4293
+ }
4294
+ }
4295
+ return false;
4296
+ }
4297
+ function classifyHorizontalOverflow({ scrollWidth, clientWidth, overflowX, hasText, isTruncated, epsilon = 1 }) {
4298
+ const overflowPx = clientWidth > 0 ? scrollWidth - clientWidth : 0;
4299
+ if (overflowPx <= epsilon) return null;
4300
+ const clipsText = hasText && (overflowX === "hidden" || overflowX === "clip") && !isTruncated;
4301
+ return { overflowPx, kind: clipsText ? "clipped-text" : "element-scroll-overflow" };
4302
+ }
4303
+ function classifyVerticalOverflow({ scrollHeight, clientHeight, overflowY, hasText, isTruncated, epsilon = 1 }) {
4304
+ const overflowPx = clientHeight > 0 ? scrollHeight - clientHeight : 0;
4305
+ if (overflowPx <= epsilon) return null;
4306
+ const scrollable = overflowY === "auto" || overflowY === "scroll";
4307
+ if (scrollable || !hasText || isTruncated) return null;
4308
+ const clips = overflowY === "hidden" || overflowY === "clip";
4309
+ return { overflowPx, kind: "clipped-text", clips };
4310
+ }
4311
+ function resolveVisibleSpillCandidates(spillCandidates, { epsilon = 1 } = {}) {
4312
+ function spillBottomEdge(candidate) {
4313
+ const explicit = Number(candidate.spillBottom);
4314
+ if (Number.isFinite(explicit)) return explicit;
4315
+ const rectBottom = Number(candidate.rect?.bottom);
4316
+ const overflowPx = Number(candidate.overflowPx);
4317
+ if (!Number.isFinite(rectBottom) || !Number.isFinite(overflowPx)) return null;
4318
+ return rectBottom + overflowPx;
4319
+ }
4320
+ function sameSpillEdge(candidate, other) {
4321
+ const candidateBottom = spillBottomEdge(candidate);
4322
+ const otherBottom = spillBottomEdge(other);
4323
+ return candidateBottom !== null && otherBottom !== null && Math.abs(candidateBottom - otherBottom) <= epsilon;
4324
+ }
4325
+ return spillCandidates.filter(
4326
+ (candidate) => !spillCandidates.some(
4327
+ (other) => other.el !== candidate.el && candidate.el.contains(other.el) && sameSpillEdge(candidate, other)
4328
+ )
4329
+ );
4330
+ }
4280
4331
  function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInteractiveControl) {
4281
4332
  let annotationMode = true;
4282
4333
  let hovered = null;
@@ -4475,11 +4526,6 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
4475
4526
  function rectArea(rect) {
4476
4527
  return Math.max(0, rect.width) * Math.max(0, rect.height);
4477
4528
  }
4478
- function intersectionArea(a, b) {
4479
- const width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
4480
- const height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
4481
- return width * height;
4482
- }
4483
4529
  function isVisibleForLayoutAudit(el, rect = el.getBoundingClientRect()) {
4484
4530
  if (!el || isLavishUi(el) || rect.width <= 0 || rect.height <= 0) return false;
4485
4531
  const style = getComputedStyle(el);
@@ -4543,31 +4589,56 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
4543
4589
  function isIntentionalTextTruncation(style) {
4544
4590
  return style.textOverflow === "ellipsis" || Number.parseInt(style.webkitLineClamp || "0", 10) > 0;
4545
4591
  }
4546
- function auditElementOverflow(el, viewportWidth, findings, seen) {
4592
+ function auditElementOverflow(el, viewportWidth, findings, seen, spillCandidates) {
4547
4593
  if (el === document.body || el === document.documentElement || hasIntentionalHorizontalScrollerAncestor(el)) return;
4548
4594
  const rect = el.getBoundingClientRect();
4549
4595
  if (!isVisibleForLayoutAudit(el, rect)) return;
4550
4596
  const style = getComputedStyle(el);
4551
- const scrollOverflowPx = el.clientWidth > 0 ? el.scrollWidth - el.clientWidth : 0;
4552
- if (scrollOverflowPx > layoutAuditOverflowEpsilon) {
4553
- const clipsText = hasReadableText(el) && (style.overflowX === "hidden" || style.overflowX === "clip") && !isIntentionalTextTruncation(style);
4597
+ const hasText = hasReadableText(el);
4598
+ const isTruncated = isIntentionalTextTruncation(style);
4599
+ const horizontal = classifyHorizontalOverflow({
4600
+ scrollWidth: el.scrollWidth,
4601
+ clientWidth: el.clientWidth,
4602
+ overflowX: style.overflowX,
4603
+ hasText,
4604
+ isTruncated,
4605
+ epsilon: layoutAuditOverflowEpsilon
4606
+ });
4607
+ if (horizontal) {
4554
4608
  pushLayoutFinding(findings, seen, {
4555
4609
  selector: selector(el),
4556
- kind: clipsText ? "clipped-text" : "element-scroll-overflow",
4557
- overflowPx: scrollOverflowPx,
4610
+ kind: horizontal.kind,
4611
+ overflowPx: horizontal.overflowPx,
4558
4612
  viewportWidth,
4559
- severity: clipsText ? "error" : overflowSeverity(scrollOverflowPx)
4613
+ severity: horizontal.kind === "clipped-text" ? "error" : overflowSeverity(horizontal.overflowPx)
4560
4614
  });
4561
4615
  }
4562
- const verticalClipPx = el.clientHeight > 0 ? el.scrollHeight - el.clientHeight : 0;
4563
- if (verticalClipPx > layoutAuditOverflowEpsilon && hasReadableText(el) && (style.overflowY === "hidden" || style.overflowY === "clip") && !isIntentionalTextTruncation(style)) {
4564
- pushLayoutFinding(findings, seen, {
4565
- selector: selector(el),
4566
- kind: "clipped-text",
4567
- overflowPx: verticalClipPx,
4568
- viewportWidth,
4569
- severity: "error"
4570
- });
4616
+ const vertical = classifyVerticalOverflow({
4617
+ scrollHeight: el.scrollHeight,
4618
+ clientHeight: el.clientHeight,
4619
+ overflowY: style.overflowY,
4620
+ hasText,
4621
+ isTruncated,
4622
+ epsilon: layoutAuditOverflowEpsilon
4623
+ });
4624
+ if (vertical) {
4625
+ if (vertical.clips) {
4626
+ pushLayoutFinding(findings, seen, {
4627
+ selector: selector(el),
4628
+ kind: vertical.kind,
4629
+ overflowPx: vertical.overflowPx,
4630
+ viewportWidth,
4631
+ severity: "error"
4632
+ });
4633
+ } else {
4634
+ spillCandidates.push({
4635
+ el,
4636
+ selector: selector(el),
4637
+ overflowPx: vertical.overflowPx,
4638
+ viewportWidth,
4639
+ spillBottom: rect.bottom + vertical.overflowPx
4640
+ });
4641
+ }
4571
4642
  }
4572
4643
  const parent2 = el.parentElement;
4573
4644
  if (!parent2 || parent2 === document.body || parent2 === document.documentElement) return;
@@ -4585,36 +4656,56 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
4585
4656
  });
4586
4657
  }
4587
4658
  }
4659
+ function resolveSpillCandidates(spillCandidates, findings, seen) {
4660
+ for (const candidate of resolveVisibleSpillCandidates(spillCandidates, { epsilon: layoutAuditOverflowEpsilon })) {
4661
+ pushLayoutFinding(findings, seen, {
4662
+ selector: candidate.selector,
4663
+ kind: "clipped-text",
4664
+ overflowPx: candidate.overflowPx,
4665
+ viewportWidth: candidate.viewportWidth,
4666
+ severity: "error"
4667
+ });
4668
+ }
4669
+ }
4670
+ function elementLineFragments(el) {
4671
+ const rects = [...el.getClientRects()].filter((r) => r.width > 0 && r.height > 0);
4672
+ if (rects.length) return rects;
4673
+ const rect = el.getBoundingClientRect();
4674
+ return rect.width > 0 && rect.height > 0 ? [rect] : [];
4675
+ }
4588
4676
  function auditOverlappingText(elements, viewportWidth, findings, seen) {
4589
- const candidates = elements.filter((el) => el.children.length === 0 && hasReadableText(el)).filter((el) => isVisibleForLayoutAudit(el)).slice(0, 200);
4677
+ const candidates = elements.filter((el) => el.children.length === 0 && hasReadableText(el)).filter((el) => isVisibleForLayoutAudit(el)).filter((el) => getComputedStyle(el).position === "static").slice(0, 200);
4590
4678
  for (const el of candidates) {
4591
- const rect = el.getBoundingClientRect();
4592
- if (rectArea(rect) < 16) continue;
4593
- const points = [
4594
- { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
4595
- { x: rect.left + Math.min(4, rect.width / 2), y: rect.top + Math.min(4, rect.height / 2) },
4596
- { x: rect.right - Math.min(4, rect.width / 2), y: rect.bottom - Math.min(4, rect.height / 2) }
4597
- ];
4598
- for (const point of points) {
4599
- if (point.x < 0 || point.y < 0 || point.x > viewportWidth || point.y > window.innerHeight) continue;
4600
- const top = document.elementFromPoint(point.x, point.y);
4601
- if (!(top instanceof Element) || top === el || el.contains(top) || top.contains(el) || isLavishUi(top))
4602
- continue;
4603
- if (hasIntentionalHorizontalScrollerAncestor(top)) continue;
4604
- const elPosition = getComputedStyle(el).position;
4605
- const topPosition = getComputedStyle(top).position;
4606
- if (elPosition !== "static" || topPosition !== "static") continue;
4607
- const topRect = top.getBoundingClientRect();
4608
- const overlapArea = intersectionArea(rect, topRect);
4609
- if (overlapArea < Math.min(rectArea(rect) * 0.25, 24)) continue;
4610
- pushLayoutFinding(findings, seen, {
4611
- selector: selector(el),
4612
- kind: "overlapping-text",
4613
- overflowPx: 0,
4614
- viewportWidth,
4615
- severity: "error"
4616
- });
4617
- break;
4679
+ const fragments = elementLineFragments(el);
4680
+ let flagged = false;
4681
+ for (const rect of fragments) {
4682
+ if (flagged) break;
4683
+ if (rectArea(rect) < 16) continue;
4684
+ const points = [
4685
+ { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
4686
+ { x: rect.left + Math.min(4, rect.width / 2), y: rect.top + Math.min(4, rect.height / 2) },
4687
+ { x: rect.right - Math.min(4, rect.width / 2), y: rect.bottom - Math.min(4, rect.height / 2) }
4688
+ ];
4689
+ for (const point of points) {
4690
+ if (point.x < 0 || point.y < 0 || point.x > viewportWidth || point.y > window.innerHeight) continue;
4691
+ const top = document.elementFromPoint(point.x, point.y);
4692
+ if (!(top instanceof Element) || top === el || el.contains(top) || top.contains(el) || isLavishUi(top))
4693
+ continue;
4694
+ if (hasIntentionalHorizontalScrollerAncestor(top)) continue;
4695
+ if (getComputedStyle(top).position !== "static") continue;
4696
+ if (!fragmentsSignificantlyOverlap([rect], elementLineFragments(top))) continue;
4697
+ pushLayoutFinding(findings, seen, {
4698
+ selector: selector(el),
4699
+ kind: "overlapping-text",
4700
+ overflowPx: 0,
4701
+ viewportWidth,
4702
+ // Heuristic and sampling-based even after fragment-aware matching, so it stays a
4703
+ // warning rather than holding the open-time gate the way a real clip/overflow does.
4704
+ severity: "warning"
4705
+ });
4706
+ flagged = true;
4707
+ break;
4708
+ }
4618
4709
  }
4619
4710
  }
4620
4711
  }
@@ -4633,7 +4724,9 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
4633
4724
  });
4634
4725
  }
4635
4726
  const elements = collectLayoutAuditElements();
4636
- for (const el of elements) auditElementOverflow(el, viewportWidth, findings, seen);
4727
+ const spillCandidates = [];
4728
+ for (const el of elements) auditElementOverflow(el, viewportWidth, findings, seen, spillCandidates);
4729
+ resolveSpillCandidates(spillCandidates, findings, seen);
4637
4730
  auditOverlappingText(elements, viewportWidth, findings, seen);
4638
4731
  return findings;
4639
4732
  }
@@ -4918,6 +5011,7 @@ var SessionStore = class {
4918
5011
  pending_prompts: existing.pending_prompts || 0,
4919
5012
  prompts: existingPrompts,
4920
5013
  layout_warnings: [],
5014
+ delivered_layout_warning_keys: existing.delivered_layout_warning_keys || [],
4921
5015
  dom_snapshot: existing.dom_snapshot || "",
4922
5016
  chat: existing.chat || [],
4923
5017
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
@@ -4950,13 +5044,23 @@ var SessionStore = class {
4950
5044
  if (!session) {
4951
5045
  return null;
4952
5046
  }
4953
- const layoutWarnings = normalizeLayoutWarnings(payload.layout_warnings || payload.layoutWarnings || []);
5047
+ const deliveredWarningKeys = session.delivered_layout_warning_keys || [];
5048
+ const deliveredKeys = new Set(deliveredWarningKeys);
5049
+ const layoutWarnings = normalizeLayoutWarnings(
5050
+ payload.layout_warnings || payload.layoutWarnings || [],
5051
+ deliveredKeys
5052
+ );
5053
+ const activeWarningKeys = new Set(layoutWarnings.map(layoutWarningKey));
5054
+ const nextDeliveredWarningKeys = deliveredWarningKeys.filter((key2) => activeWarningKeys.has(key2)).slice(-200);
5055
+ const deliveredKeysChanged = nextDeliveredWarningKeys.length !== deliveredWarningKeys.length || nextDeliveredWarningKeys.some((key2, index) => key2 !== deliveredWarningKeys[index]);
4954
5056
  const previousSignature = JSON.stringify(session.layout_warnings || []);
4955
5057
  const nextSignature = JSON.stringify(layoutWarnings);
4956
- if (previousSignature === nextSignature) {
5058
+ const warningsChanged = previousSignature !== nextSignature;
5059
+ if (!warningsChanged && !deliveredKeysChanged) {
4957
5060
  return { session, changed: false, hasWarnings: layoutWarnings.length > 0 };
4958
5061
  }
4959
5062
  session.layout_warnings = layoutWarnings;
5063
+ session.delivered_layout_warning_keys = nextDeliveredWarningKeys;
4960
5064
  if (layoutWarnings.length > 0 && session.status !== "ended") {
4961
5065
  session.status = "feedback";
4962
5066
  } else if ((session.prompts || []).length === 0 && session.status !== "ended") {
@@ -4964,7 +5068,7 @@ var SessionStore = class {
4964
5068
  }
4965
5069
  session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
4966
5070
  await this.writeState(state);
4967
- return { session, changed: true, hasWarnings: layoutWarnings.length > 0 };
5071
+ return { session, changed: warningsChanged, hasWarnings: layoutWarnings.length > 0 };
4968
5072
  }
4969
5073
  async takeFeedback(key) {
4970
5074
  const state = await this.readState();
@@ -4987,6 +5091,11 @@ var SessionStore = class {
4987
5091
  session.layout_warnings = [];
4988
5092
  session.pending_prompts = 0;
4989
5093
  session.dom_snapshot = "";
5094
+ if (layoutWarnings.length > 0) {
5095
+ const deliveredKeys = new Set(session.delivered_layout_warning_keys || []);
5096
+ for (const warning of layoutWarnings) deliveredKeys.add(layoutWarningKey(warning));
5097
+ session.delivered_layout_warning_keys = [...deliveredKeys].slice(-200);
5098
+ }
4990
5099
  if (session.status !== "ended") {
4991
5100
  session.status = "open";
4992
5101
  }
@@ -5052,15 +5161,23 @@ function normalizePrompt(prompt) {
5052
5161
  if (target) normalized.target = target;
5053
5162
  return normalized;
5054
5163
  }
5055
- function normalizeLayoutWarnings(layoutWarnings) {
5164
+ function layoutWarningKey(warning) {
5165
+ return `${warning.kind}:${warning.selector}`;
5166
+ }
5167
+ function normalizeLayoutWarnings(layoutWarnings, deliveredKeys = /* @__PURE__ */ new Set()) {
5056
5168
  if (!Array.isArray(layoutWarnings)) return [];
5057
- return layoutWarnings.filter((warning) => warning && typeof warning === "object" && !Array.isArray(warning)).map((warning) => ({
5058
- selector: String(warning.selector || ""),
5059
- kind: String(warning.kind || "layout-warning"),
5060
- overflowPx: normalizeFiniteNumber(warning.overflowPx),
5061
- viewportWidth: normalizeFiniteNumber(warning.viewportWidth),
5062
- severity: warning.severity === "warning" ? "warning" : "error"
5063
- }));
5169
+ return layoutWarnings.filter((warning) => warning && typeof warning === "object" && !Array.isArray(warning)).map((warning) => {
5170
+ const selector = String(warning.selector || "");
5171
+ const kind = String(warning.kind || "layout-warning");
5172
+ return {
5173
+ selector,
5174
+ kind,
5175
+ overflowPx: normalizeFiniteNumber(warning.overflowPx),
5176
+ viewportWidth: normalizeFiniteNumber(warning.viewportWidth),
5177
+ severity: warning.severity === "warning" ? "warning" : "error",
5178
+ persistent: deliveredKeys.has(layoutWarningKey({ kind, selector }))
5179
+ };
5180
+ });
5064
5181
  }
5065
5182
  function normalizeFiniteNumber(value) {
5066
5183
  const number = Number(value);
@@ -5818,6 +5935,10 @@ const key=${JSON.stringify(key)};
5818
5935
  void key;
5819
5936
  const deriveQueueKey=${deriveLavishQueueKey.toString()};
5820
5937
  const isNativeInteractiveControl=${isNativeInteractiveControl.toString()};
5938
+ const fragmentsSignificantlyOverlap=${fragmentsSignificantlyOverlap.toString()};
5939
+ const resolveVisibleSpillCandidates=${resolveVisibleSpillCandidates.toString()};
5940
+ const classifyHorizontalOverflow=${classifyHorizontalOverflow.toString()};
5941
+ const classifyVerticalOverflow=${classifyVerticalOverflow.toString()};
5821
5942
  (${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl);
5822
5943
  })();`;
5823
5944
  }
@@ -6004,7 +6125,7 @@ function normalizePagePath(path6) {
6004
6125
  var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
6005
6126
  var RESERVED = new Set(RESERVED_COMMANDS);
6006
6127
  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`.";
6007
- var VERSION = "0.1.33";
6128
+ var VERSION = "0.1.34";
6008
6129
  async function run(argv) {
6009
6130
  await ensureStateDir();
6010
6131
  const normalizedArgv = normalizeArgv(argv);
@@ -6099,7 +6220,7 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
6099
6220
  "Run `lavish-axi <html-file>` to open or resume a Lavish Editor session",
6100
6221
  "Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
6101
6222
  "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",
6102
- "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 layout_warnings before involving the human. 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",
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",
6103
6224
  "Run `lavish-axi end <html-file>` to end a session",
6104
6225
  "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",
6105
6226
  "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",
@@ -6129,7 +6250,7 @@ function createPlaybookOutput(args) {
6129
6250
  function createOpenOutput({ file, url, status }) {
6130
6251
  return {
6131
6252
  session: { file, url, status },
6132
- 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, fix overflow, clipped text, or overlapping unreadable content and re-check before involving the human. 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.`
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.`
6133
6254
  };
6134
6255
  }
6135
6256
  async function openCommand(args) {
@@ -6234,7 +6355,7 @@ function createPollOutput({ file, response }) {
6234
6355
  dom_snapshot: response.dom_snapshot || "",
6235
6356
  prompts: response.prompts || [],
6236
6357
  ...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
6237
- next_step: createFeedbackNextStep(file, layoutWarnings.length)
6358
+ next_step: createFeedbackNextStep(file, layoutWarnings)
6238
6359
  };
6239
6360
  }
6240
6361
  if (response.status === "ended") {
@@ -6245,10 +6366,28 @@ function createPollOutput({ file, response }) {
6245
6366
  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.`
6246
6367
  };
6247
6368
  }
6248
- function createFeedbackNextStep(file, layoutWarningCount) {
6249
- const layoutPrefix = layoutWarningCount > 0 ? `${layoutWarningCount} layout warning${layoutWarningCount === 1 ? "" : "s"} detected - fix horizontal overflow, clipped text, or overlapping unreadable content in ${file}, then reload or re-open the artifact and re-check before involving the human. ` : `Apply the requested changes to ${file}. `;
6369
+ function createFeedbackNextStep(file, layoutWarnings) {
6370
+ const count = layoutWarnings.length;
6371
+ const layoutPrefix = count > 0 ? layoutWarningsPrefix(file, layoutWarnings) : `Apply the requested changes to ${file}. `;
6250
6372
  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.`;
6251
6373
  }
6374
+ function layoutWarningsPrefix(file, layoutWarnings) {
6375
+ const count = layoutWarnings.length;
6376
+ const plural = count === 1 ? "" : "s";
6377
+ const allPersistent = layoutWarnings.every((warning) => warning.persistent);
6378
+ const allLowSeverity = layoutWarnings.every((warning) => warning.severity !== "error");
6379
+ const allRepeatOrLowSeverity = layoutWarnings.every((warning) => warning.persistent || warning.severity !== "error");
6380
+ if (allPersistent) {
6381
+ return `${count} layout warning${plural} detected, and every one was already reported in a prior poll and is still unresolved - if you already attempted a fix, it is fine to proceed to the human with a short note about what remains instead of looping further edits and reloads. `;
6382
+ }
6383
+ if (allLowSeverity) {
6384
+ return `${count} low-severity layout warning${plural} detected (no error-severity findings) - fix them if the cause is obvious in ${file}, otherwise it is fine to proceed to the human with a note instead of iterating further. `;
6385
+ }
6386
+ if (allRepeatOrLowSeverity) {
6387
+ 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
+ }
6389
+ return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then reload or re-open the artifact and re-check before involving the human. `;
6390
+ }
6252
6391
  async function endCommand(args) {
6253
6392
  const file = firstPositionalArg(args);
6254
6393
  if (!file) {
@@ -6783,7 +6922,7 @@ Usage:
6783
6922
 
6784
6923
  ${DESIGN_SYSTEM_HINT}
6785
6924
 
6786
- 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 layout_warnings before involving the human. 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.
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.
6787
6926
 
6788
6927
  `;
6789
6928
  var COMMAND_HELP = {
@@ -6793,7 +6932,7 @@ Open or resume a Lavish Editor review session for an HTML artifact. Use --no-ope
6793
6932
  `,
6794
6933
  poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."]
6795
6934
 
6796
- 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 layout_warnings before involving the human. 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.
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.
6797
6936
  `,
6798
6937
  end: `Usage: lavish-axi end <html-file>
6799
6938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
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",
@@ -34,7 +34,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac
34
34
  3. Run `npx -y lavish-axi poll <html-file>` to long-poll for the user's annotations, queued prompts, and browser-reported `layout_warnings`.
35
35
  The poll stays silent until the user acts or the real browser reports fresh layout warnings - leave it running, never kill it.
36
36
  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.
37
- 4. If poll returns `layout_warnings`, fix overflow, clipped text, or overlapping unreadable content and re-check before involving the human.
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
40
 
@@ -65,7 +65,7 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an
65
65
  - Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session
66
66
  - Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`
67
67
  - 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 layout_warnings before involving the human. 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
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
69
  - Run `npx -y lavish-axi end <html-file>` to end a session
70
70
  - 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
71
  - 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