lavish-axi 0.1.33 → 0.1.35
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 +14 -7
- package/dist/chrome-client.js +21 -6
- package/dist/cli.mjs +280 -87
- package/package.json +1 -1
- package/skills/lavish/SKILL.md +5 -4
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
|
-
|
|
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.
|
|
@@ -153,10 +155,14 @@ pnpm link
|
|
|
153
155
|
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
154
156
|
For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
|
|
155
157
|
Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
|
|
156
|
-
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which
|
|
158
|
+
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which sends the prompts and user-ended attribution together.
|
|
157
159
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
158
160
|
In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately.
|
|
159
161
|
- **Agent presence** - The browser shows when no agent is listening, keeps queued feedback and fresh layout warnings for the next successful `lavish-axi poll` send even across reloads, and only blocks human sends while the agent is working on delivered feedback. The no-timeout poll writes an immediate stderr banner and periodic stderr heartbeats while stdout stays reserved for the final response; if the poll is interrupted or times out, re-run it because queued feedback is never lost.
|
|
162
|
+
- **Session end etiquette** - Lavish tracks who ended a session: a human clicking **End session** (or **Send & end session**) in the browser is a user-initiated end, while `lavish-axi end <html-file>` is agent-initiated.
|
|
163
|
+
A plain `lavish-axi <html-file>` after a user-initiated end refuses to reopen the browser and returns guidance instead; pass `--reopen` only when the user asks for further review or something important needs their visual attention.
|
|
164
|
+
Agent-initiated ends keep reopening normally, same as before.
|
|
165
|
+
`lavish-axi poll`'s `ended` response and the `feedback` response for the final batch before an end both carry `next_step` guidance telling the agent to stop polling and deliver remaining updates in chat instead of reopening.
|
|
160
166
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
161
167
|
- **Server cleanup** - The detached server stops after the last session ends when nothing is connected, or after `LAVISH_AXI_IDLE_TIMEOUT_MS` (default 30 minutes) with no browser or poll connections.
|
|
162
168
|
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
@@ -169,9 +175,9 @@ pnpm link
|
|
|
169
175
|
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
170
176
|
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
171
177
|
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
172
|
-
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default.
|
|
173
|
-
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted.
|
|
174
|
-
| `lavish-axi end <html-file>` | End a session.
|
|
178
|
+
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
|
|
179
|
+
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. On `status: ended`, stop polling and do not reopen uninvited. |
|
|
180
|
+
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later. |
|
|
175
181
|
| `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links. |
|
|
176
182
|
| `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default, and `--password` makes viewers enter the password before viewing. |
|
|
177
183
|
| `lavish-axi stop` | Shut down the background server. |
|
|
@@ -190,6 +196,7 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
190
196
|
| ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
191
197
|
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
192
198
|
| `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
|
|
199
|
+
| `lavish-axi <html-file>` | `--reopen` | Reopen a session the user explicitly ended from the browser; without it, a plain open refuses and explains why instead of reopening uninvited. |
|
|
193
200
|
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
194
201
|
| `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
|
|
195
202
|
| `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private; viewers must supply the password. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -328,21 +328,26 @@ async function submitQueued() {
|
|
|
328
328
|
submitQueuedAgain = false;
|
|
329
329
|
if (!succeeded) {
|
|
330
330
|
endAfterSubmit = false;
|
|
331
|
-
} else if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
endAfterSubmit
|
|
335
|
-
|
|
331
|
+
} else if (!ended && shouldSubmitAgain) {
|
|
332
|
+
if (queued.length) {
|
|
333
|
+
submitQueued();
|
|
334
|
+
} else if (endAfterSubmit) {
|
|
335
|
+
endAfterSubmit = false;
|
|
336
|
+
endSession();
|
|
337
|
+
}
|
|
336
338
|
}
|
|
337
339
|
}
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
async function submitQueuedOnce() {
|
|
341
343
|
const prompts = queued.slice();
|
|
344
|
+
const shouldEndSession = endAfterSubmit;
|
|
345
|
+
const body = { prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot };
|
|
346
|
+
if (shouldEndSession) body.endSession = true;
|
|
342
347
|
const response = await fetch("/api/" + key + "/prompts", {
|
|
343
348
|
method: "POST",
|
|
344
349
|
headers: { "content-type": "application/json" },
|
|
345
|
-
body: JSON.stringify(
|
|
350
|
+
body: JSON.stringify(body),
|
|
346
351
|
});
|
|
347
352
|
if (!response.ok) throw new Error("failed to submit queued prompts");
|
|
348
353
|
for (const prompt of prompts) {
|
|
@@ -351,6 +356,11 @@ async function submitQueuedOnce() {
|
|
|
351
356
|
}
|
|
352
357
|
persistQueuedPrompts();
|
|
353
358
|
render();
|
|
359
|
+
if (shouldEndSession) {
|
|
360
|
+
endAfterSubmit = false;
|
|
361
|
+
markSessionEnded();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
354
364
|
if (agentPresence === "listening") setAgentPresence("working");
|
|
355
365
|
}
|
|
356
366
|
|
|
@@ -474,6 +484,11 @@ async function endSession() {
|
|
|
474
484
|
if (ended) return;
|
|
475
485
|
const response = await fetch("/api/" + key + "/end", { method: "POST" });
|
|
476
486
|
if (!response.ok) throw new Error("failed to end session");
|
|
487
|
+
markSessionEnded();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function markSessionEnded() {
|
|
491
|
+
if (ended) return;
|
|
477
492
|
ended = true;
|
|
478
493
|
closeMenus();
|
|
479
494
|
annotationSwitch.disabled = true;
|
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
|
|
4552
|
-
|
|
4553
|
-
|
|
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:
|
|
4557
|
-
overflowPx:
|
|
4610
|
+
kind: horizontal.kind,
|
|
4611
|
+
overflowPx: horizontal.overflowPx,
|
|
4558
4612
|
viewportWidth,
|
|
4559
|
-
severity:
|
|
4613
|
+
severity: horizontal.kind === "clipped-text" ? "error" : overflowSeverity(horizontal.overflowPx)
|
|
4560
4614
|
});
|
|
4561
4615
|
}
|
|
4562
|
-
const
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
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
|
|
4592
|
-
|
|
4593
|
-
const
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
continue;
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -4933,13 +5027,16 @@ var SessionStore = class {
|
|
|
4933
5027
|
return null;
|
|
4934
5028
|
}
|
|
4935
5029
|
const prompts = Array.isArray(payload.prompts) ? payload.prompts : [];
|
|
5030
|
+
const shouldEndSession = Boolean(payload.endSession || payload.end_session);
|
|
5031
|
+
const alreadyEnded = session.status === "ended";
|
|
4936
5032
|
const normalizedPrompts = prompts.map(normalizePrompt);
|
|
4937
5033
|
const userMessages = normalizedPrompts.filter((prompt) => prompt.tag === "message" && prompt.prompt).map((prompt) => ({ role: "user", text: prompt.prompt, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
4938
5034
|
session.prompts = [...session.prompts || [], ...normalizedPrompts];
|
|
4939
5035
|
session.chat = [...session.chat || [], ...userMessages];
|
|
4940
5036
|
session.pending_prompts = session.prompts.length;
|
|
4941
5037
|
session.dom_snapshot = String(payload.domSnapshot || payload.dom_snapshot || "");
|
|
4942
|
-
session.status = "feedback";
|
|
5038
|
+
session.status = shouldEndSession || alreadyEnded ? "ended" : "feedback";
|
|
5039
|
+
if (shouldEndSession) session.ended_by = "user";
|
|
4943
5040
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4944
5041
|
await this.writeState(state);
|
|
4945
5042
|
return session;
|
|
@@ -4950,13 +5047,23 @@ var SessionStore = class {
|
|
|
4950
5047
|
if (!session) {
|
|
4951
5048
|
return null;
|
|
4952
5049
|
}
|
|
4953
|
-
const
|
|
5050
|
+
const deliveredWarningKeys = session.delivered_layout_warning_keys || [];
|
|
5051
|
+
const deliveredKeys = new Set(deliveredWarningKeys);
|
|
5052
|
+
const layoutWarnings = normalizeLayoutWarnings(
|
|
5053
|
+
payload.layout_warnings || payload.layoutWarnings || [],
|
|
5054
|
+
deliveredKeys
|
|
5055
|
+
);
|
|
5056
|
+
const activeWarningKeys = new Set(layoutWarnings.map(layoutWarningKey));
|
|
5057
|
+
const nextDeliveredWarningKeys = deliveredWarningKeys.filter((key2) => activeWarningKeys.has(key2)).slice(-200);
|
|
5058
|
+
const deliveredKeysChanged = nextDeliveredWarningKeys.length !== deliveredWarningKeys.length || nextDeliveredWarningKeys.some((key2, index) => key2 !== deliveredWarningKeys[index]);
|
|
4954
5059
|
const previousSignature = JSON.stringify(session.layout_warnings || []);
|
|
4955
5060
|
const nextSignature = JSON.stringify(layoutWarnings);
|
|
4956
|
-
|
|
5061
|
+
const warningsChanged = previousSignature !== nextSignature;
|
|
5062
|
+
if (!warningsChanged && !deliveredKeysChanged) {
|
|
4957
5063
|
return { session, changed: false, hasWarnings: layoutWarnings.length > 0 };
|
|
4958
5064
|
}
|
|
4959
5065
|
session.layout_warnings = layoutWarnings;
|
|
5066
|
+
session.delivered_layout_warning_keys = nextDeliveredWarningKeys;
|
|
4960
5067
|
if (layoutWarnings.length > 0 && session.status !== "ended") {
|
|
4961
5068
|
session.status = "feedback";
|
|
4962
5069
|
} else if ((session.prompts || []).length === 0 && session.status !== "ended") {
|
|
@@ -4964,7 +5071,7 @@ var SessionStore = class {
|
|
|
4964
5071
|
}
|
|
4965
5072
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4966
5073
|
await this.writeState(state);
|
|
4967
|
-
return { session, changed:
|
|
5074
|
+
return { session, changed: warningsChanged, hasWarnings: layoutWarnings.length > 0 };
|
|
4968
5075
|
}
|
|
4969
5076
|
async takeFeedback(key) {
|
|
4970
5077
|
const state = await this.readState();
|
|
@@ -4974,33 +5081,48 @@ var SessionStore = class {
|
|
|
4974
5081
|
}
|
|
4975
5082
|
const prompts = session.prompts || [];
|
|
4976
5083
|
const layoutWarnings = session.layout_warnings || [];
|
|
5084
|
+
const alreadyEnded = session.status === "ended";
|
|
4977
5085
|
if (prompts.length === 0 && layoutWarnings.length === 0) {
|
|
4978
|
-
return
|
|
5086
|
+
return alreadyEnded ? { status: "ended", ended_by: session.ended_by } : { status: "waiting" };
|
|
4979
5087
|
}
|
|
4980
5088
|
const result = {
|
|
4981
5089
|
status: "feedback",
|
|
4982
5090
|
dom_snapshot: session.dom_snapshot || "",
|
|
4983
5091
|
prompts,
|
|
4984
|
-
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {}
|
|
5092
|
+
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
|
|
5093
|
+
// This is the final delivery before the session shows as ended - flag it so the agent
|
|
5094
|
+
// knows not to expect (or force) a reopened browser afterward.
|
|
5095
|
+
...alreadyEnded ? { session_ended: true, ended_by: session.ended_by } : {}
|
|
4985
5096
|
};
|
|
4986
5097
|
session.prompts = [];
|
|
4987
5098
|
session.layout_warnings = [];
|
|
4988
5099
|
session.pending_prompts = 0;
|
|
4989
5100
|
session.dom_snapshot = "";
|
|
4990
|
-
if (
|
|
5101
|
+
if (layoutWarnings.length > 0) {
|
|
5102
|
+
const deliveredKeys = new Set(session.delivered_layout_warning_keys || []);
|
|
5103
|
+
for (const warning of layoutWarnings) deliveredKeys.add(layoutWarningKey(warning));
|
|
5104
|
+
session.delivered_layout_warning_keys = [...deliveredKeys].slice(-200);
|
|
5105
|
+
}
|
|
5106
|
+
if (!alreadyEnded) {
|
|
4991
5107
|
session.status = "open";
|
|
4992
5108
|
}
|
|
4993
5109
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4994
5110
|
await this.writeState(state);
|
|
4995
5111
|
return result;
|
|
4996
5112
|
}
|
|
4997
|
-
|
|
5113
|
+
// `endedBy` distinguishes a human ending review from the browser chrome ("user") from an
|
|
5114
|
+
// agent explicitly closing the loop via `lavish-axi end` ("agent"). Only a user-initiated end
|
|
5115
|
+
// blocks a plain reopen - see `SessionStore` callers in server.js.
|
|
5116
|
+
async endSession(key, endedBy = "agent") {
|
|
4998
5117
|
const state = await this.readState();
|
|
4999
5118
|
const session = state.sessions[key];
|
|
5000
5119
|
if (!session) {
|
|
5001
5120
|
return null;
|
|
5002
5121
|
}
|
|
5122
|
+
const existingEndedBy = session.status === "ended" ? session.ended_by : void 0;
|
|
5123
|
+
const nextEndedBy = endedBy === "user" || existingEndedBy === "user" ? "user" : "agent";
|
|
5003
5124
|
session.status = "ended";
|
|
5125
|
+
session.ended_by = nextEndedBy;
|
|
5004
5126
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5005
5127
|
await this.writeState(state);
|
|
5006
5128
|
return session;
|
|
@@ -5052,15 +5174,23 @@ function normalizePrompt(prompt) {
|
|
|
5052
5174
|
if (target) normalized.target = target;
|
|
5053
5175
|
return normalized;
|
|
5054
5176
|
}
|
|
5055
|
-
function
|
|
5177
|
+
function layoutWarningKey(warning) {
|
|
5178
|
+
return `${warning.kind}:${warning.selector}`;
|
|
5179
|
+
}
|
|
5180
|
+
function normalizeLayoutWarnings(layoutWarnings, deliveredKeys = /* @__PURE__ */ new Set()) {
|
|
5056
5181
|
if (!Array.isArray(layoutWarnings)) return [];
|
|
5057
|
-
return layoutWarnings.filter((warning) => warning && typeof warning === "object" && !Array.isArray(warning)).map((warning) =>
|
|
5058
|
-
selector
|
|
5059
|
-
kind
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5182
|
+
return layoutWarnings.filter((warning) => warning && typeof warning === "object" && !Array.isArray(warning)).map((warning) => {
|
|
5183
|
+
const selector = String(warning.selector || "");
|
|
5184
|
+
const kind = String(warning.kind || "layout-warning");
|
|
5185
|
+
return {
|
|
5186
|
+
selector,
|
|
5187
|
+
kind,
|
|
5188
|
+
overflowPx: normalizeFiniteNumber(warning.overflowPx),
|
|
5189
|
+
viewportWidth: normalizeFiniteNumber(warning.viewportWidth),
|
|
5190
|
+
severity: warning.severity === "warning" ? "warning" : "error",
|
|
5191
|
+
persistent: deliveredKeys.has(layoutWarningKey({ kind, selector }))
|
|
5192
|
+
};
|
|
5193
|
+
});
|
|
5064
5194
|
}
|
|
5065
5195
|
function normalizeFiniteNumber(value) {
|
|
5066
5196
|
const number = Number(value);
|
|
@@ -5139,9 +5269,15 @@ async function serve({
|
|
|
5139
5269
|
try {
|
|
5140
5270
|
const file = await canonicalFile(req.body.file);
|
|
5141
5271
|
const key = sessionKey(file);
|
|
5272
|
+
const reopen = Boolean(req.body.reopen);
|
|
5273
|
+
const existing = await store.findByKey(key);
|
|
5274
|
+
if (existing?.status === "ended" && existing.ended_by === "user" && !reopen) {
|
|
5275
|
+
logEvent?.(`session open blocked (user-ended) key=${key} file=${file}`);
|
|
5276
|
+
res.json({ key, file, url: existing.url, status: "user-ended" });
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5142
5279
|
const sessionUrl = `http://${hostForUrl(linkHostName)}:${publicPort}/session/${key}`;
|
|
5143
5280
|
const url = shouldDisableLayoutGateOpen(req.body || {}) ? appendNoGateParam(sessionUrl) : sessionUrl;
|
|
5144
|
-
const existing = await store.findByKey(key);
|
|
5145
5281
|
const session = await store.upsertSession(file, sessionUrl);
|
|
5146
5282
|
if (existing?.status === "ended") {
|
|
5147
5283
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
@@ -5227,13 +5363,16 @@ async function serve({
|
|
|
5227
5363
|
});
|
|
5228
5364
|
app.post("/api/:key/prompts", async (req, res, next) => {
|
|
5229
5365
|
try {
|
|
5366
|
+
const shouldEndSession = Boolean(req.body?.endSession || req.body?.end_session);
|
|
5230
5367
|
const session = await store.queuePrompts(req.params.key, req.body || {});
|
|
5231
5368
|
if (!session) {
|
|
5232
5369
|
res.status(404).json({ error: "session not found" });
|
|
5233
5370
|
return;
|
|
5234
5371
|
}
|
|
5235
|
-
|
|
5372
|
+
if (shouldEndSession) clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
5373
|
+
events.emit(shouldEndSession ? "ended" : "feedback", req.params.key);
|
|
5236
5374
|
res.json({ status: "queued", pending_prompts: session.pending_prompts });
|
|
5375
|
+
if (shouldEndSession) await shutdownIfNoLiveSessions();
|
|
5237
5376
|
} catch (error) {
|
|
5238
5377
|
next(error);
|
|
5239
5378
|
}
|
|
@@ -5255,7 +5394,7 @@ async function serve({
|
|
|
5255
5394
|
});
|
|
5256
5395
|
app.post("/api/:key/end", async (req, res, next) => {
|
|
5257
5396
|
try {
|
|
5258
|
-
await store.endSession(req.params.key);
|
|
5397
|
+
await store.endSession(req.params.key, "user");
|
|
5259
5398
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
5260
5399
|
events.emit("ended", req.params.key);
|
|
5261
5400
|
res.json({ status: "ended" });
|
|
@@ -5342,7 +5481,7 @@ async function serve({
|
|
|
5342
5481
|
try {
|
|
5343
5482
|
const file = await canonicalFile(req.body.file);
|
|
5344
5483
|
const key = sessionKey(file);
|
|
5345
|
-
await store.endSession(key);
|
|
5484
|
+
await store.endSession(key, "agent");
|
|
5346
5485
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
5347
5486
|
events.emit("ended", key);
|
|
5348
5487
|
res.json({ status: "ended" });
|
|
@@ -5818,6 +5957,10 @@ const key=${JSON.stringify(key)};
|
|
|
5818
5957
|
void key;
|
|
5819
5958
|
const deriveQueueKey=${deriveLavishQueueKey.toString()};
|
|
5820
5959
|
const isNativeInteractiveControl=${isNativeInteractiveControl.toString()};
|
|
5960
|
+
const fragmentsSignificantlyOverlap=${fragmentsSignificantlyOverlap.toString()};
|
|
5961
|
+
const resolveVisibleSpillCandidates=${resolveVisibleSpillCandidates.toString()};
|
|
5962
|
+
const classifyHorizontalOverflow=${classifyHorizontalOverflow.toString()};
|
|
5963
|
+
const classifyVerticalOverflow=${classifyVerticalOverflow.toString()};
|
|
5821
5964
|
(${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl);
|
|
5822
5965
|
})();`;
|
|
5823
5966
|
}
|
|
@@ -6004,7 +6147,7 @@ function normalizePagePath(path6) {
|
|
|
6004
6147
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
|
|
6005
6148
|
var RESERVED = new Set(RESERVED_COMMANDS);
|
|
6006
6149
|
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.
|
|
6150
|
+
var VERSION = "0.1.35";
|
|
6008
6151
|
async function run(argv) {
|
|
6009
6152
|
await ensureStateDir();
|
|
6010
6153
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -6096,11 +6239,11 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
|
|
|
6096
6239
|
],
|
|
6097
6240
|
playbooks: listPlaybooks(),
|
|
6098
6241
|
help: [
|
|
6099
|
-
"Run `lavish-axi <html-file>` to open or resume a Lavish Editor session",
|
|
6242
|
+
"Run `lavish-axi <html-file>` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention",
|
|
6100
6243
|
"Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
|
|
6101
6244
|
"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",
|
|
6103
|
-
"Run `lavish-axi end <html-file>` to end a session",
|
|
6245
|
+
"Run `lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead",
|
|
6246
|
+
"Run `lavish-axi end <html-file>` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `lavish-axi <html-file>` refuses to reopen it without `--reopen`",
|
|
6104
6247
|
"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
6248
|
"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",
|
|
6106
6249
|
"Run `lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)",
|
|
@@ -6129,7 +6272,13 @@ function createPlaybookOutput(args) {
|
|
|
6129
6272
|
function createOpenOutput({ file, url, status }) {
|
|
6130
6273
|
return {
|
|
6131
6274
|
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
|
|
6275
|
+
next_step: `Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback, ends the session, or the real browser reports layout_warnings from the in-iframe layout audit, and it stays silent the whole time - that is normal, never kill it. If layout_warnings arrive, follow the poll response's next_step: fix and re-check fresh error-severity overflow or clipped-text findings before involving the human, but persistent or low-severity warnings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if the poll still gets killed or times out, just re-run it - queued feedback is never lost. After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show your response in Lavish Editor and wait for more feedback. If the user ends the session, stop polling and do not reopen it by re-running \`lavish-axi ${file}\` unless the user asks for further review or something genuinely important needs their visual attention - deliver routine updates directly in this conversation instead. When reopening is warranted, run \`lavish-axi ${file} --reopen\`.`
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6278
|
+
function createUserEndedOpenOutput({ file, url }) {
|
|
6279
|
+
return {
|
|
6280
|
+
session: { file, url, status: "user-ended" },
|
|
6281
|
+
next_step: `The user explicitly ended this Lavish Editor session from the browser, so \`lavish-axi ${file}\` did not reopen it. Do not reopen unless the user asks for further review or something genuinely important needs their visual attention - deliver routine updates directly in this conversation instead. When reopening is warranted, run \`lavish-axi ${file} --reopen\`.`
|
|
6133
6282
|
};
|
|
6134
6283
|
}
|
|
6135
6284
|
async function openCommand(args) {
|
|
@@ -6140,8 +6289,12 @@ async function openCommand(args) {
|
|
|
6140
6289
|
await assertHtmlFile(file);
|
|
6141
6290
|
const absolute = await canonicalFile(file);
|
|
6142
6291
|
const noGate = args.includes("--no-gate");
|
|
6292
|
+
const reopen = args.includes("--reopen");
|
|
6143
6293
|
const baseUrl = await ensureServer({ forceRestart: shouldForceRestartForLocalBuild(process.argv[1] || "") });
|
|
6144
|
-
const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate });
|
|
6294
|
+
const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate, reopen });
|
|
6295
|
+
if (response.status === "user-ended") {
|
|
6296
|
+
return createUserEndedOpenOutput({ file: absolute, url: response.url });
|
|
6297
|
+
}
|
|
6145
6298
|
if (shouldOpenBrowser(args, process.env)) {
|
|
6146
6299
|
try {
|
|
6147
6300
|
const open = (await import("open")).default;
|
|
@@ -6229,26 +6382,66 @@ function createPollOutput({ file, response }) {
|
|
|
6229
6382
|
}
|
|
6230
6383
|
if (response.status === "feedback") {
|
|
6231
6384
|
const layoutWarnings = Array.isArray(response.layout_warnings) ? response.layout_warnings : [];
|
|
6385
|
+
const sessionEnded = Boolean(response.session_ended);
|
|
6386
|
+
const endedBy = typeof response.ended_by === "string" ? response.ended_by : void 0;
|
|
6232
6387
|
return {
|
|
6233
|
-
session: {
|
|
6388
|
+
session: {
|
|
6389
|
+
file,
|
|
6390
|
+
status: "feedback",
|
|
6391
|
+
...sessionEnded ? { session_ended: true, ...endedBy ? { ended_by: endedBy } : {} } : {}
|
|
6392
|
+
},
|
|
6234
6393
|
dom_snapshot: response.dom_snapshot || "",
|
|
6235
6394
|
prompts: response.prompts || [],
|
|
6236
6395
|
...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
|
|
6237
|
-
next_step: createFeedbackNextStep(file, layoutWarnings
|
|
6396
|
+
next_step: createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy)
|
|
6238
6397
|
};
|
|
6239
6398
|
}
|
|
6240
6399
|
if (response.status === "ended") {
|
|
6241
|
-
return {
|
|
6400
|
+
return {
|
|
6401
|
+
session: { file, status: "ended", ...response.ended_by ? { ended_by: response.ended_by } : {} },
|
|
6402
|
+
next_step: createEndedNextStep(file, response.ended_by)
|
|
6403
|
+
};
|
|
6242
6404
|
}
|
|
6243
6405
|
return {
|
|
6244
6406
|
session: { file, status: response.status || "waiting" },
|
|
6245
6407
|
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
6408
|
};
|
|
6247
6409
|
}
|
|
6248
|
-
function createFeedbackNextStep(file,
|
|
6249
|
-
const
|
|
6410
|
+
function createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy) {
|
|
6411
|
+
const count = layoutWarnings.length;
|
|
6412
|
+
if (sessionEnded) {
|
|
6413
|
+
const layoutNote = count > 0 ? `${count} layout warning${count === 1 ? "" : "s"} arrived alongside this final feedback. ` : "";
|
|
6414
|
+
if (endedBy === "user") {
|
|
6415
|
+
return `${layoutNote}This was the last feedback before the user ended the session. Stop polling ${file} and do not reopen it - deliver any remaining updates directly in this conversation instead. Only run \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
|
|
6416
|
+
}
|
|
6417
|
+
return `${layoutNote}This was the last feedback before the Lavish Editor session ended. Stop polling ${file}. Deliver any remaining updates directly in this conversation, or run \`lavish-axi ${file}\` to open a fresh session if the user needs further visual review.`;
|
|
6418
|
+
}
|
|
6419
|
+
const layoutPrefix = count > 0 ? layoutWarningsPrefix(file, layoutWarnings) : `Apply the requested changes to ${file}. `;
|
|
6250
6420
|
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
6421
|
}
|
|
6422
|
+
function layoutWarningsPrefix(file, layoutWarnings) {
|
|
6423
|
+
const count = layoutWarnings.length;
|
|
6424
|
+
const plural = count === 1 ? "" : "s";
|
|
6425
|
+
const allPersistent = layoutWarnings.every((warning) => warning.persistent);
|
|
6426
|
+
const allLowSeverity = layoutWarnings.every((warning) => warning.severity !== "error");
|
|
6427
|
+
const allRepeatOrLowSeverity = layoutWarnings.every((warning) => warning.persistent || warning.severity !== "error");
|
|
6428
|
+
if (allPersistent) {
|
|
6429
|
+
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. `;
|
|
6430
|
+
}
|
|
6431
|
+
if (allLowSeverity) {
|
|
6432
|
+
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. `;
|
|
6433
|
+
}
|
|
6434
|
+
if (allRepeatOrLowSeverity) {
|
|
6435
|
+
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. `;
|
|
6436
|
+
}
|
|
6437
|
+
return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then re-check in the browser before involving the human. Lavish live-reloads the artifact automatically after you save, so you do not need to re-run \`lavish-axi ${file}\` for this. `;
|
|
6438
|
+
}
|
|
6439
|
+
function createEndedNextStep(file, endedBy) {
|
|
6440
|
+
if (endedBy === "user") {
|
|
6441
|
+
return `The user ended this Lavish Editor session. Stop polling ${file} - do not run \`lavish-axi ${file}\` to reopen it. Deliver any remaining updates directly in this conversation instead. Only reopen with \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
|
|
6442
|
+
}
|
|
6443
|
+
return `This Lavish Editor session for ${file} has ended. Stop polling. Deliver any remaining updates directly in this conversation, or run \`lavish-axi ${file}\` to open a fresh session if the user needs further visual review.`;
|
|
6444
|
+
}
|
|
6252
6445
|
async function endCommand(args) {
|
|
6253
6446
|
const file = firstPositionalArg(args);
|
|
6254
6447
|
if (!file) {
|
|
@@ -6771,7 +6964,7 @@ var TOP_LEVEL_HELP = `lavish-axi - Lavish Editor AXI
|
|
|
6771
6964
|
|
|
6772
6965
|
Usage:
|
|
6773
6966
|
lavish-axi
|
|
6774
|
-
lavish-axi <html-file> [--no-open] [--no-gate]
|
|
6967
|
+
lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
6775
6968
|
lavish-axi poll <html-file> [--agent-reply "..."]
|
|
6776
6969
|
lavish-axi end <html-file>
|
|
6777
6970
|
lavish-axi export <html-file> [--out <path>]
|
|
@@ -6783,21 +6976,21 @@ Usage:
|
|
|
6783
6976
|
|
|
6784
6977
|
${DESIGN_SYSTEM_HINT}
|
|
6785
6978
|
|
|
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.
|
|
6979
|
+
Note: poll long-polls indefinitely by default until the user sends feedback, ends the session, or the browser reports fresh layout_warnings, staying silent while it waits - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When the user ends a session from the browser, stop polling and do not reopen it uninvited - pass --reopen to <html-file> only when the user asks for further review or something important needs their visual attention.
|
|
6787
6980
|
|
|
6788
6981
|
`;
|
|
6789
6982
|
var COMMAND_HELP = {
|
|
6790
|
-
open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate]
|
|
6983
|
+
open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
|
|
6791
6984
|
|
|
6792
|
-
Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open.
|
|
6985
|
+
Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open. If the user explicitly ended the session from the browser, this refuses to reopen it and returns guidance instead - pass --reopen to force it open when the user asks for further review or something important needs their visual attention. Sessions ended by the agent (\`lavish-axi end\`) reopen normally without the flag.
|
|
6793
6986
|
`,
|
|
6794
6987
|
poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."]
|
|
6795
6988
|
|
|
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.
|
|
6989
|
+
This command long-polls indefinitely for queued user prompts and browser-reported layout_warnings, then returns them to the agent. It stays silent while it waits - that is normal, never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if it still gets killed or times out, just re-run it - queued feedback is never lost. Use --agent-reply after applying prior feedback to display your response in Lavish Editor before waiting again. When status is ended, stop polling and do not reopen the session uninvited - deliver remaining updates directly in this conversation instead.
|
|
6797
6990
|
`,
|
|
6798
6991
|
end: `Usage: lavish-axi end <html-file>
|
|
6799
6992
|
|
|
6800
|
-
End a Lavish Editor session.
|
|
6993
|
+
End a Lavish Editor session as the agent. A session ended this way still reopens normally on the next \`lavish-axi <html-file>\`, unlike a user ending it from the browser, which requires --reopen.
|
|
6801
6994
|
`,
|
|
6802
6995
|
export: `Usage: lavish-axi export <html-file> [--out <path>]
|
|
6803
6996
|
|
package/package.json
CHANGED
package/skills/lavish/SKILL.md
CHANGED
|
@@ -34,9 +34,10 @@ 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
|
|
37
|
+
4. If poll returns `layout_warnings`, follow the returned `next_step`: fix and re-check fresh error-severity findings, but proceed with a note instead of looping when every current warning is persistent or low-severity.
|
|
38
38
|
5. Apply human feedback, then poll again with `--agent-reply "<message>"` to reply in the browser and keep the loop going.
|
|
39
39
|
6. Run `npx -y lavish-axi end <html-file>` when the review is finished.
|
|
40
|
+
7. If the user ends the session from the browser instead, `npx -y lavish-axi <html-file>` refuses to reopen it and says so - only pass `--reopen` when the user asks for further review or something genuinely important needs their visual attention. Otherwise deliver remaining updates directly in this conversation.
|
|
40
41
|
|
|
41
42
|
## Visual guidance
|
|
42
43
|
|
|
@@ -62,11 +63,11 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an
|
|
|
62
63
|
|
|
63
64
|
## Commands & rules
|
|
64
65
|
|
|
65
|
-
- Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session
|
|
66
|
+
- Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention
|
|
66
67
|
- Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`
|
|
67
68
|
- Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work
|
|
68
|
-
- Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix 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
|
|
69
|
-
- Run `npx -y lavish-axi end <html-file>` to end a session
|
|
69
|
+
- Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead
|
|
70
|
+
- Run `npx -y lavish-axi end <html-file>` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `npx -y lavish-axi <html-file>` refuses to reopen it without `--reopen`
|
|
70
71
|
- Run `npx -y lavish-axi export <html-file> [--out <path>]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu
|
|
71
72
|
- Run `npx -y lavish-axi share <html-file> [--password <pw>] [--token <t>]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu
|
|
72
73
|
- Run `npx -y lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)
|