lavish-axi 0.1.75 → 0.1.77
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 +4 -0
- package/dist/chrome-client.js +288 -0
- package/dist/chrome.css +152 -0
- package/dist/cli.mjs +208 -3
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/lavish/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -181,6 +181,10 @@ pnpm link
|
|
|
181
181
|
`Resolved` requires a newer successful artifact load plus a complete check at the same viewport that no longer detects it; it then leaves the count but keeps a bounded history.
|
|
182
182
|
`Still present` (recurring) means a queued issue survived a newer revision, so it is selectable again with its earlier attempt retained. `Unverified` means a reload or check failed or was incomplete, so the prior issue was preserved rather than cleared. `Returned` means a resolved issue came back on a later revision.
|
|
183
183
|
Dismissal applies only to the current artifact revision; a later revision surfaces the issue again if it is still detected. A check at one viewport never clears an issue found at another, and a viewport removed from the configured diagnostic set (`LAVISH_AXI_DIAGNOSTIC_VIEWPORTS`, default all) is marked obsolete with an explicit reason rather than reading as fixed.
|
|
184
|
+
- **Revisions legend** - Opt-in. After a round of feedback the agent regenerates the whole file, and a one-sentence edit is invisible on a long artifact. The agent can declare what it changed in the artifact itself: one `<script type="application/json" data-lavish-revisions>` array of `{ id, label, timestamp, summary }` entries, plus `data-lavish-revision="<id>"` on each block it edited or added. Run `lavish-axi design` for the snippet.
|
|
185
|
+
A **Revisions** button then appears in the top bar with a legend listing each round - colour swatch, label, time, and the agent's summary - and **Reveal** steps through that round's changed blocks, flashing them one at a time.
|
|
186
|
+
Each revision's swatch carries a distinct border style and fill pattern as well as a colour, so the rounds stay distinguishable without colour. Artifacts with no registry show no button and are otherwise unaffected, a malformed registry is ignored rather than surfaced as an error, and at most 6 revisions are listed - one per distinguishable swatch.
|
|
187
|
+
Lavish never marks up the artifact for this: the legend lives in the browser chrome, so the served page stays identical to the saved file, and the registry and marks travel with the file through export.
|
|
184
188
|
- **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.
|
|
185
189
|
- **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.
|
|
186
190
|
`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/chrome-client.js
CHANGED
|
@@ -178,6 +178,12 @@ const warningsSelectAll = /** @type {HTMLInputElement} */ (document.getElementBy
|
|
|
178
178
|
const warningsSelected = /** @type {HTMLSpanElement} */ (document.getElementById("warningsSelected"));
|
|
179
179
|
const warningsList = /** @type {HTMLDivElement} */ (document.getElementById("warningsList"));
|
|
180
180
|
const warningsQueueButton = /** @type {HTMLButtonElement} */ (document.getElementById("warningsQueueButton"));
|
|
181
|
+
const revisionsWrap = /** @type {HTMLDivElement} */ (document.getElementById("revisionsWrap"));
|
|
182
|
+
const revisionsButton = /** @type {HTMLButtonElement} */ (document.getElementById("revisionsButton"));
|
|
183
|
+
const revisionsCount = /** @type {HTMLSpanElement} */ (document.getElementById("revisionsCount"));
|
|
184
|
+
const revisionsDrawer = /** @type {HTMLDivElement} */ (document.getElementById("revisionsDrawer"));
|
|
185
|
+
const revisionsSummary = /** @type {HTMLParagraphElement} */ (document.getElementById("revisionsSummary"));
|
|
186
|
+
const revisionsList = /** @type {HTMLDivElement} */ (document.getElementById("revisionsList"));
|
|
181
187
|
const sendHint = /** @type {HTMLDivElement} */ (document.getElementById("sendHint"));
|
|
182
188
|
const whiteboardOverlay = /** @type {HTMLDivElement} */ (document.getElementById("whiteboardOverlay"));
|
|
183
189
|
const whiteboardFrame = /** @type {HTMLIFrameElement} */ (document.getElementById("whiteboardFrame"));
|
|
@@ -2431,6 +2437,265 @@ function revealWarning(warning) {
|
|
|
2431
2437
|
postToFrame({ type: "lavish:revealElement", selector: warning.selector });
|
|
2432
2438
|
}
|
|
2433
2439
|
|
|
2440
|
+
// ---------------------------------------------------------------------------
|
|
2441
|
+
// Revision legend
|
|
2442
|
+
//
|
|
2443
|
+
// The agent declares what it changed in the artifact HTML itself (a
|
|
2444
|
+
// `data-lavish-revisions` registry plus `data-lavish-revision` marks); the SDK
|
|
2445
|
+
// reads it and posts it here. The legend lives entirely in the chrome, so the
|
|
2446
|
+
// served artifact still matches the file on disk - Lavish never paints the
|
|
2447
|
+
// change indicator into the page. Reveal borrows the same transient marker the
|
|
2448
|
+
// layout-issues drawer uses, and only when the reader asks for it.
|
|
2449
|
+
|
|
2450
|
+
// The payload crosses postMessage from a sandboxed frame rendering author
|
|
2451
|
+
// content, so it is untrusted here whatever built it. These bound what is
|
|
2452
|
+
// examined, not what is accepted: capping accepted rows alone would let a
|
|
2453
|
+
// registry of ten thousand records walk the whole array on the chrome's main
|
|
2454
|
+
// thread before yielding its handful of rows.
|
|
2455
|
+
// Server-owned, injected into the session JSON from src/artifact-revisions.js.
|
|
2456
|
+
// The swatch is presentation the chrome owes the reader, not something the
|
|
2457
|
+
// artifact gets a say in, so nothing about it is read from the message.
|
|
2458
|
+
const REVISION_PALETTE = (Array.isArray(sessionData.revisionPalette) ? sessionData.revisionPalette : []).filter(
|
|
2459
|
+
(entry) => entry && /^#[0-9a-fA-F]{6}$/.test(String(entry.hex)),
|
|
2460
|
+
);
|
|
2461
|
+
const REVISION_LIMITS = {
|
|
2462
|
+
// Derived from the server-injected palette (src/artifact-revisions.js) rather than a
|
|
2463
|
+
// second hardcoded number, so the two can never drift; falls back to 6 only if the
|
|
2464
|
+
// palette itself came through empty.
|
|
2465
|
+
entries: REVISION_PALETTE.length > 0 ? REVISION_PALETTE.length : 6,
|
|
2466
|
+
rawEntries: 256,
|
|
2467
|
+
marks: 200,
|
|
2468
|
+
rawMarks: 2000,
|
|
2469
|
+
id: 60,
|
|
2470
|
+
label: 80,
|
|
2471
|
+
summary: 400,
|
|
2472
|
+
timestamp: 40,
|
|
2473
|
+
excerpt: 120,
|
|
2474
|
+
selector: 400,
|
|
2475
|
+
};
|
|
2476
|
+
const REVISION_BORDER_STYLES = ["solid", "dashed", "dotted", "double"];
|
|
2477
|
+
|
|
2478
|
+
/** @type {any[]} */
|
|
2479
|
+
let revisionEntries = [];
|
|
2480
|
+
/** @type {any[]} */
|
|
2481
|
+
let revisionMarks = [];
|
|
2482
|
+
/** @type {Map<string, number>} */
|
|
2483
|
+
const revisionRevealCursor = new Map();
|
|
2484
|
+
let revisionsDrawerOpen = false;
|
|
2485
|
+
|
|
2486
|
+
// Display-only fields. Anything the legend merely shows can be shortened.
|
|
2487
|
+
function revisionText(value, max) {
|
|
2488
|
+
if (typeof value !== "string" && typeof value !== "number") return "";
|
|
2489
|
+
const text = String(value).trim();
|
|
2490
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// Identity fields, mirroring `exactRevisionText` in src/artifact-revisions.js.
|
|
2494
|
+
// The message is untrusted here, so shortening an id or a selector is worse
|
|
2495
|
+
// than dropping it: two distinct overlong ids collapse into one legend row, and
|
|
2496
|
+
// the first 400 characters of a long selector are a different valid selector
|
|
2497
|
+
// that Reveal would resolve to some other block.
|
|
2498
|
+
function revisionExact(value, max) {
|
|
2499
|
+
if (typeof value !== "string" && typeof value !== "number") return "";
|
|
2500
|
+
const text = String(value).trim();
|
|
2501
|
+
return text.length > 0 && text.length <= max ? text : "";
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function revisionPatternFill(pattern) {
|
|
2505
|
+
if (pattern === "diagonal") return "repeating-linear-gradient(45deg, currentColor 0 2px, transparent 2px 5px)";
|
|
2506
|
+
if (pattern === "dots") return "radial-gradient(currentColor 1px, transparent 1px)";
|
|
2507
|
+
return "";
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// The swatch for a revision's position in the registry. Position is the only
|
|
2511
|
+
// thing the artifact influences, and it cannot repeat one: ids are deduplicated
|
|
2512
|
+
// before this runs, so each accepted revision gets a distinct index and
|
|
2513
|
+
// therefore a distinct swatch.
|
|
2514
|
+
function revisionPresentation(index) {
|
|
2515
|
+
const entry = REVISION_PALETTE.length > 0 ? REVISION_PALETTE[index % REVISION_PALETTE.length] : null;
|
|
2516
|
+
return {
|
|
2517
|
+
color: entry ? String(entry.hex) : "#0072b2",
|
|
2518
|
+
borderStyle: REVISION_BORDER_STYLES.includes(String(entry && entry.borderStyle))
|
|
2519
|
+
? String(entry.borderStyle)
|
|
2520
|
+
: "solid",
|
|
2521
|
+
pattern: revisionPatternFill(entry && entry.pattern),
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// Only text crosses from the artifact into the legend, and it crosses as
|
|
2526
|
+
// textContent. Nothing the message carries reaches a style property.
|
|
2527
|
+
function normalizeRevisionMessage(msg) {
|
|
2528
|
+
const rawRevisions = Array.isArray(msg && msg.revisions) ? msg.revisions : [];
|
|
2529
|
+
const revisions = [];
|
|
2530
|
+
const byId = new Map();
|
|
2531
|
+
let examined = 0;
|
|
2532
|
+
for (const raw of rawRevisions) {
|
|
2533
|
+
if (examined >= REVISION_LIMITS.rawEntries || revisions.length >= REVISION_LIMITS.entries) break;
|
|
2534
|
+
examined += 1;
|
|
2535
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2536
|
+
const id = revisionExact(raw.id, REVISION_LIMITS.id);
|
|
2537
|
+
if (!id || /\s/.test(id) || byId.has(id)) continue;
|
|
2538
|
+
const entry = {
|
|
2539
|
+
id,
|
|
2540
|
+
label: revisionText(raw.label, REVISION_LIMITS.label) || id,
|
|
2541
|
+
timestamp: revisionText(raw.timestamp, REVISION_LIMITS.timestamp),
|
|
2542
|
+
summary: revisionText(raw.summary, REVISION_LIMITS.summary),
|
|
2543
|
+
...revisionPresentation(revisions.length),
|
|
2544
|
+
marks: [],
|
|
2545
|
+
};
|
|
2546
|
+
byId.set(id, entry);
|
|
2547
|
+
revisions.push(entry);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
const rawMarks = Array.isArray(msg && msg.marks) ? msg.marks : [];
|
|
2551
|
+
const marks = [];
|
|
2552
|
+
examined = 0;
|
|
2553
|
+
for (const raw of rawMarks) {
|
|
2554
|
+
if (examined >= REVISION_LIMITS.rawMarks || marks.length >= REVISION_LIMITS.marks) break;
|
|
2555
|
+
examined += 1;
|
|
2556
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2557
|
+
const revisionId = revisionExact(raw.revision_id, REVISION_LIMITS.id);
|
|
2558
|
+
const selector = revisionExact(raw.selector, REVISION_LIMITS.selector);
|
|
2559
|
+
const owner = byId.get(revisionId);
|
|
2560
|
+
if (!owner || !selector) continue;
|
|
2561
|
+
const mark = {
|
|
2562
|
+
revisionId,
|
|
2563
|
+
selector,
|
|
2564
|
+
tag: revisionText(raw.tag, 40).toLowerCase(),
|
|
2565
|
+
excerpt: revisionText(raw.excerpt, REVISION_LIMITS.excerpt),
|
|
2566
|
+
};
|
|
2567
|
+
owner.marks.push(mark);
|
|
2568
|
+
marks.push(mark);
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
return { revisions, marks };
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function resetRevisionLegend() {
|
|
2575
|
+
revisionEntries = [];
|
|
2576
|
+
revisionMarks = [];
|
|
2577
|
+
revisionRevealCursor.clear();
|
|
2578
|
+
renderRevisionLegend();
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function applyRevisionMessage(msg) {
|
|
2582
|
+
const normalized = normalizeRevisionMessage(msg);
|
|
2583
|
+
revisionEntries = normalized.revisions;
|
|
2584
|
+
revisionMarks = normalized.marks;
|
|
2585
|
+
for (const id of [...revisionRevealCursor.keys()]) {
|
|
2586
|
+
if (!revisionEntries.some((entry) => entry.id === id)) revisionRevealCursor.delete(id);
|
|
2587
|
+
}
|
|
2588
|
+
renderRevisionLegend();
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
function revisionMarkSummary(entry) {
|
|
2592
|
+
if (entry.marks.length === 0) return "no marked blocks";
|
|
2593
|
+
return entry.marks.length === 1 ? "1 marked block" : entry.marks.length + " marked blocks";
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
function buildRevisionRow(entry) {
|
|
2597
|
+
const row = document.createElement("div");
|
|
2598
|
+
row.className = "revision-row";
|
|
2599
|
+
|
|
2600
|
+
const swatch = document.createElement("span");
|
|
2601
|
+
swatch.className = "revision-swatch";
|
|
2602
|
+
swatch.setAttribute("aria-hidden", "true");
|
|
2603
|
+
swatch.style.color = entry.color;
|
|
2604
|
+
swatch.style.borderColor = entry.color;
|
|
2605
|
+
swatch.style.borderStyle = entry.borderStyle;
|
|
2606
|
+
if (entry.pattern) swatch.style.backgroundImage = entry.pattern;
|
|
2607
|
+
row.appendChild(swatch);
|
|
2608
|
+
|
|
2609
|
+
const body = document.createElement("div");
|
|
2610
|
+
body.className = "revision-body";
|
|
2611
|
+
|
|
2612
|
+
const head = document.createElement("div");
|
|
2613
|
+
head.className = "revision-head";
|
|
2614
|
+
const label = document.createElement("span");
|
|
2615
|
+
label.className = "revision-label";
|
|
2616
|
+
label.textContent = entry.label;
|
|
2617
|
+
head.appendChild(label);
|
|
2618
|
+
if (entry.timestamp) {
|
|
2619
|
+
const time = document.createElement("span");
|
|
2620
|
+
time.className = "revision-time";
|
|
2621
|
+
time.textContent = entry.timestamp;
|
|
2622
|
+
head.appendChild(time);
|
|
2623
|
+
}
|
|
2624
|
+
body.appendChild(head);
|
|
2625
|
+
|
|
2626
|
+
if (entry.summary) {
|
|
2627
|
+
const summary = document.createElement("p");
|
|
2628
|
+
summary.className = "revision-summary";
|
|
2629
|
+
summary.textContent = entry.summary;
|
|
2630
|
+
body.appendChild(summary);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
const foot = document.createElement("div");
|
|
2634
|
+
foot.className = "revision-foot";
|
|
2635
|
+
const count = document.createElement("span");
|
|
2636
|
+
count.className = "revision-marks";
|
|
2637
|
+
count.textContent = revisionMarkSummary(entry);
|
|
2638
|
+
foot.appendChild(count);
|
|
2639
|
+
if (entry.marks.length > 0) {
|
|
2640
|
+
const reveal = document.createElement("button");
|
|
2641
|
+
reveal.type = "button";
|
|
2642
|
+
reveal.className = "revision-reveal";
|
|
2643
|
+
const position = (revisionRevealCursor.get(entry.id) || 0) % entry.marks.length;
|
|
2644
|
+
reveal.textContent = entry.marks.length === 1 ? "Reveal" : "Reveal " + (position + 1) + "/" + entry.marks.length;
|
|
2645
|
+
reveal.setAttribute("aria-label", "Reveal the next block changed in " + entry.label);
|
|
2646
|
+
// Bound per row rather than delegated from the list: the rows are rebuilt
|
|
2647
|
+
// on every render anyway, so there is no listener to accumulate, and the
|
|
2648
|
+
// button carries the revision it belongs to without a data attribute round
|
|
2649
|
+
// trip through the DOM.
|
|
2650
|
+
reveal.onclick = () => revealNextRevisionMark(entry.id);
|
|
2651
|
+
foot.appendChild(reveal);
|
|
2652
|
+
}
|
|
2653
|
+
body.appendChild(foot);
|
|
2654
|
+
|
|
2655
|
+
row.appendChild(body);
|
|
2656
|
+
return row;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
function renderRevisionLegend() {
|
|
2660
|
+
if (!revisionsWrap) return;
|
|
2661
|
+
const count = revisionEntries.length;
|
|
2662
|
+
revisionsWrap.hidden = count === 0 || ended;
|
|
2663
|
+
if (revisionsWrap.hidden && revisionsDrawerOpen) setRevisionsDrawerOpen(false);
|
|
2664
|
+
revisionsCount.textContent = String(count);
|
|
2665
|
+
revisionsButton.setAttribute("aria-label", count === 1 ? "1 revision" : count + " revisions");
|
|
2666
|
+
revisionsSummary.textContent =
|
|
2667
|
+
(count === 1 ? "1 revision" : count + " revisions") +
|
|
2668
|
+
" · " +
|
|
2669
|
+
(revisionMarks.length === 1 ? "1 marked block" : revisionMarks.length + " marked blocks");
|
|
2670
|
+
|
|
2671
|
+
revisionsList.replaceChildren(...revisionEntries.map(buildRevisionRow));
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function setRevisionsDrawerOpen(open) {
|
|
2675
|
+
revisionsDrawerOpen = open && !ended;
|
|
2676
|
+
revisionsDrawer.hidden = !revisionsDrawerOpen;
|
|
2677
|
+
revisionsButton.setAttribute("aria-expanded", String(revisionsDrawerOpen));
|
|
2678
|
+
if (revisionsDrawerOpen) closeMenus();
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function closeRevisionsDrawer({ restoreFocus = false } = {}) {
|
|
2682
|
+
if (!revisionsDrawerOpen) return;
|
|
2683
|
+
setRevisionsDrawerOpen(false);
|
|
2684
|
+
if (restoreFocus) revisionsButton.focus();
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// Step through a revision's marked blocks one at a time. The existing reveal
|
|
2688
|
+
// path flashes one element and clears the previous marker, so a revision that
|
|
2689
|
+
// touched several blocks is walked rather than lit up all at once.
|
|
2690
|
+
function revealNextRevisionMark(id) {
|
|
2691
|
+
const entry = revisionEntries.find((candidate) => candidate.id === id);
|
|
2692
|
+
if (!entry || entry.marks.length === 0) return;
|
|
2693
|
+
const position = (revisionRevealCursor.get(id) || 0) % entry.marks.length;
|
|
2694
|
+
postToFrame({ type: "lavish:revealElement", selector: entry.marks[position].selector });
|
|
2695
|
+
revisionRevealCursor.set(id, (position + 1) % entry.marks.length);
|
|
2696
|
+
renderRevisionLegend();
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2434
2699
|
async function dismissWarning(id) {
|
|
2435
2700
|
try {
|
|
2436
2701
|
const response = await fetch("/api/" + key + "/layout-warnings/dismiss", {
|
|
@@ -2530,6 +2795,8 @@ function markSessionEnded() {
|
|
|
2530
2795
|
closeShareDialog();
|
|
2531
2796
|
closeWarningsDrawer();
|
|
2532
2797
|
renderWarnings();
|
|
2798
|
+
closeRevisionsDrawer();
|
|
2799
|
+
renderRevisionLegend();
|
|
2533
2800
|
closeWhiteboard();
|
|
2534
2801
|
annotationSwitch.disabled = true;
|
|
2535
2802
|
moreButton.disabled = true;
|
|
@@ -2837,6 +3104,11 @@ async function replaceArtifactFrame({ recoveryRetry = false } = {}) {
|
|
|
2837
3104
|
clearTimeout(artifactSilenceTimer);
|
|
2838
3105
|
// The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
|
|
2839
3106
|
if (!artifactSrc) {
|
|
3107
|
+
// The next document reports its own registry once it loads; until then the
|
|
3108
|
+
// previous revision's legend would point at blocks that may no longer exist.
|
|
3109
|
+
// Only clear it here, right before the frame is actually replaced - a preserved
|
|
3110
|
+
// load (superseded/out-of-order/exhausted retries below) must leave it intact.
|
|
3111
|
+
resetRevisionLegend();
|
|
2840
3112
|
startLayoutGateCycle();
|
|
2841
3113
|
const currentSrc = frame.src || "about:blank";
|
|
2842
3114
|
frame.src = currentSrc + (currentSrc.includes("?") ? "&" : "?") + "lavish_reload=" + Date.now();
|
|
@@ -2953,6 +3225,9 @@ async function replaceArtifactFrame({ recoveryRetry = false } = {}) {
|
|
|
2953
3225
|
inlineWhiteboardChannels.clear();
|
|
2954
3226
|
setHandoffSuperseded(false);
|
|
2955
3227
|
startLayoutGateCycle();
|
|
3228
|
+
// The next document reports its own registry once it loads; until then the
|
|
3229
|
+
// previous revision's legend would point at blocks that may no longer exist.
|
|
3230
|
+
resetRevisionLegend();
|
|
2956
3231
|
frame.src = artifactFrameSrcForLoad({ revision, token });
|
|
2957
3232
|
return true;
|
|
2958
3233
|
}
|
|
@@ -3688,6 +3963,7 @@ window.addEventListener("message", (event) => {
|
|
|
3688
3963
|
// chrome cannot see every live reference, so reclamation is the sweeper's job.
|
|
3689
3964
|
if (msg.type === "lavish:sendQueuedPrompts") sendQueued();
|
|
3690
3965
|
if (msg.type === "lavish:endSession") endSession();
|
|
3966
|
+
if (msg.type === "lavish:revisions") applyRevisionMessage(msg);
|
|
3691
3967
|
if (msg.type === "lavish:toggleAnnotationMode") toggleAnnotationMode();
|
|
3692
3968
|
});
|
|
3693
3969
|
|
|
@@ -3831,9 +4107,14 @@ sendButton.onclick = () => sendQueued(false);
|
|
|
3831
4107
|
sendAndEndButton.onclick = () => sendQueued(true);
|
|
3832
4108
|
moreButton.onclick = () => {
|
|
3833
4109
|
closeWarningsDrawer();
|
|
4110
|
+
closeRevisionsDrawer();
|
|
3834
4111
|
toggleMenu(moreButton, moreMenu);
|
|
3835
4112
|
};
|
|
3836
4113
|
warningsButton.onclick = toggleWarningsDrawer;
|
|
4114
|
+
revisionsButton.onclick = () => {
|
|
4115
|
+
closeWarningsDrawer();
|
|
4116
|
+
setRevisionsDrawerOpen(revisionsDrawer.hidden);
|
|
4117
|
+
};
|
|
3837
4118
|
warningsSelectAll.onchange = toggleSelectAllWarnings;
|
|
3838
4119
|
warningsQueueButton.onclick = queueSelectedWarningFixes;
|
|
3839
4120
|
chatAttachButton.onclick = () => chatAttachInput.click();
|
|
@@ -3963,6 +4244,7 @@ document.addEventListener("mousedown", (event) => {
|
|
|
3963
4244
|
const target = /** @type {Node} */ (event.target);
|
|
3964
4245
|
if (!moreMenu.hidden && !moreWrap.contains(target)) setMenuOpen(moreButton, moreMenu, false);
|
|
3965
4246
|
if (warningsDrawerOpen && !warningsWrap.contains(target)) closeWarningsDrawer();
|
|
4247
|
+
if (revisionsDrawerOpen && !revisionsWrap.contains(target)) closeRevisionsDrawer();
|
|
3966
4248
|
});
|
|
3967
4249
|
// A non-modal popover closes when focus leaves it, so keyboard users are never stranded inside a
|
|
3968
4250
|
// panel they cannot see the end of.
|
|
@@ -3970,6 +4252,10 @@ warningsWrap.addEventListener("focusout", (event) => {
|
|
|
3970
4252
|
const next = /** @type {Node | null} */ (event.relatedTarget);
|
|
3971
4253
|
if (warningsDrawerOpen && next && !warningsWrap.contains(next)) closeWarningsDrawer();
|
|
3972
4254
|
});
|
|
4255
|
+
revisionsWrap.addEventListener("focusout", (event) => {
|
|
4256
|
+
const next = /** @type {Node | null} */ (event.relatedTarget);
|
|
4257
|
+
if (revisionsDrawerOpen && next && !revisionsWrap.contains(next)) closeRevisionsDrawer();
|
|
4258
|
+
});
|
|
3973
4259
|
whiteboardCloseButton.onclick = closeWhiteboard;
|
|
3974
4260
|
document.addEventListener("keydown", (event) => {
|
|
3975
4261
|
if (event.key === "Escape") {
|
|
@@ -3977,6 +4263,8 @@ document.addEventListener("keydown", (event) => {
|
|
|
3977
4263
|
closeWhiteboard();
|
|
3978
4264
|
} else if (!shareDialog.hidden) {
|
|
3979
4265
|
closeShareDialog();
|
|
4266
|
+
} else if (revisionsDrawerOpen) {
|
|
4267
|
+
closeRevisionsDrawer({ restoreFocus: true });
|
|
3980
4268
|
} else if (warningsDrawerOpen) {
|
|
3981
4269
|
closeWarningsDrawer({ restoreFocus: true });
|
|
3982
4270
|
} else if (!moreMenu.hidden) {
|
package/dist/chrome.css
CHANGED
|
@@ -635,6 +635,158 @@ body.lavish {
|
|
|
635
635
|
opacity: 0.8;
|
|
636
636
|
flex-shrink: 0;
|
|
637
637
|
}
|
|
638
|
+
.revisions-wrap {
|
|
639
|
+
position: relative;
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: center;
|
|
642
|
+
}
|
|
643
|
+
.revisions-wrap[hidden] {
|
|
644
|
+
display: none;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.revisions-button {
|
|
648
|
+
display: inline-flex;
|
|
649
|
+
align-items: center;
|
|
650
|
+
gap: 6px;
|
|
651
|
+
height: 34px;
|
|
652
|
+
padding: 0 10px;
|
|
653
|
+
border-radius: var(--radius-md);
|
|
654
|
+
border: 1px solid var(--border);
|
|
655
|
+
background: var(--bg-panel);
|
|
656
|
+
color: var(--fg);
|
|
657
|
+
font: 600 12px/1 var(--font-sans);
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.revisions-button:hover {
|
|
662
|
+
background: var(--bg-elevated);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.revisions-count {
|
|
666
|
+
min-width: 18px;
|
|
667
|
+
padding: 0 5px;
|
|
668
|
+
border-radius: 9px;
|
|
669
|
+
background: var(--accent);
|
|
670
|
+
color: var(--brass-ink);
|
|
671
|
+
font: 700 11px/18px var(--font-sans);
|
|
672
|
+
text-align: center;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.revisions-drawer {
|
|
676
|
+
right: 0;
|
|
677
|
+
top: 42px;
|
|
678
|
+
width: min(360px, calc(100vw - 24px));
|
|
679
|
+
padding: 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.revisions-head {
|
|
683
|
+
padding: 12px 14px 8px;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.revisions-title {
|
|
687
|
+
margin: 0;
|
|
688
|
+
font-size: 13px;
|
|
689
|
+
font-weight: 700;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.revisions-summary {
|
|
693
|
+
margin: 4px 0 0;
|
|
694
|
+
font-size: 11px;
|
|
695
|
+
color: var(--fg-faint);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.revisions-list {
|
|
699
|
+
max-height: 320px;
|
|
700
|
+
overflow-y: auto;
|
|
701
|
+
padding: 0 14px;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.revision-row {
|
|
705
|
+
display: flex;
|
|
706
|
+
gap: 10px;
|
|
707
|
+
padding: 10px 0;
|
|
708
|
+
border-top: 1px solid var(--border);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* Colour is never the only signal: the swatch also carries the revision's
|
|
712
|
+
border style and fill pattern, so the legend still separates revisions in
|
|
713
|
+
greyscale or with a colour vision deficiency. */
|
|
714
|
+
.revision-swatch {
|
|
715
|
+
flex: 0 0 auto;
|
|
716
|
+
width: 16px;
|
|
717
|
+
height: 16px;
|
|
718
|
+
margin-top: 2px;
|
|
719
|
+
border-width: 2px;
|
|
720
|
+
border-radius: 4px;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.revision-body {
|
|
724
|
+
min-width: 0;
|
|
725
|
+
flex: 1 1 auto;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.revision-head {
|
|
729
|
+
display: flex;
|
|
730
|
+
align-items: baseline;
|
|
731
|
+
gap: 8px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.revision-label {
|
|
735
|
+
font-size: 12px;
|
|
736
|
+
font-weight: 700;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.revision-time {
|
|
740
|
+
font-size: 11px;
|
|
741
|
+
color: var(--fg-faint);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.revision-summary {
|
|
745
|
+
margin: 4px 0 0;
|
|
746
|
+
font-size: 11px;
|
|
747
|
+
line-height: 1.45;
|
|
748
|
+
color: var(--fg-faint);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.revision-foot {
|
|
752
|
+
display: flex;
|
|
753
|
+
align-items: center;
|
|
754
|
+
justify-content: space-between;
|
|
755
|
+
gap: 8px;
|
|
756
|
+
margin-top: 6px;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.revision-marks {
|
|
760
|
+
font-size: 11px;
|
|
761
|
+
color: var(--fg-faint);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.revision-reveal {
|
|
765
|
+
border: 1px solid var(--border);
|
|
766
|
+
border-radius: 8px;
|
|
767
|
+
background: var(--bg-elevated);
|
|
768
|
+
color: var(--fg);
|
|
769
|
+
padding: 4px 8px;
|
|
770
|
+
font: 700 11px/1 var(--font-sans);
|
|
771
|
+
cursor: pointer;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.revision-reveal:hover {
|
|
775
|
+
background: var(--steel-700);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.revisions-foot {
|
|
779
|
+
padding: 10px 14px 12px;
|
|
780
|
+
border-top: 1px solid var(--border);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.revisions-note {
|
|
784
|
+
margin: 0;
|
|
785
|
+
font-size: 11px;
|
|
786
|
+
line-height: 1.45;
|
|
787
|
+
color: var(--fg-faint);
|
|
788
|
+
}
|
|
789
|
+
|
|
638
790
|
.menu-item.danger {
|
|
639
791
|
color: var(--danger);
|
|
640
792
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -453,6 +453,10 @@ var LAYOUT_SAFETY_CSS_SNIPPET = `<style>
|
|
|
453
453
|
height: auto;
|
|
454
454
|
}
|
|
455
455
|
</style>`;
|
|
456
|
+
var REVISION_REGISTRY_SNIPPET = `<script type="application/json" data-lavish-revisions>
|
|
457
|
+
[{ "id": "r1", "label": "Round 1", "timestamp": "2026-01-01T10:00:00Z", "summary": "Tightened the pricing copy" }]
|
|
458
|
+
</script>
|
|
459
|
+
<section data-lavish-revision="r1">...the block you changed...</section>`;
|
|
456
460
|
var DESIGN_PRIORITY_RULE = "Decide the design direction in this strict priority order, and only move to the next step when the current one truly yields nothing: (1) if the user asked for a specific look or named design system, use that; (2) otherwise you must first inspect the project the artifact is about - the subject or product whose content or UI it represents, which may differ from your current working directory - and match that project's design system: Tailwind or theme config, shared CSS variables or design tokens, component library, brand assets, or existing styled pages. If the artifact previews, proposes, or mocks a specific app's UI, render it in that app's own design system so it faithfully shows the product, even when you are running in a different repo; (3) only when both steps come up empty, use the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN, and prefer that CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user.";
|
|
457
461
|
var DESIGN_SYSTEM_HINT = "Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Before writing any HTML: " + DESIGN_PRIORITY_RULE + " Run `lavish-axi design` for a content-to-playbook router, a copy-pasteable CDN snippet, the whiteboard (Mermaid) opt-in snippet, and the DaisyUI component reference. When you deliver the artifact, state which of the three design sources you used and why.";
|
|
458
462
|
var DAISYUI_THEMES = [
|
|
@@ -515,6 +519,11 @@ function createDesignOutput() {
|
|
|
515
519
|
cdn_urls: { mermaid: MERMAID_CDN_URL },
|
|
516
520
|
versions: { mermaid: MERMAID_VERSION }
|
|
517
521
|
},
|
|
522
|
+
revision_marking: {
|
|
523
|
+
use_when: "Opt-in only: on a regeneration that answers the reviewer's feedback, declare what you changed so the browser can show a Revisions legend. Skip it on a first draft, and skip it when you rewrote the whole artifact - a legend that marks everything says nothing.",
|
|
524
|
+
registry_snippet: REVISION_REGISTRY_SNIPPET,
|
|
525
|
+
how: 'Append one entry per round to the `data-lavish-revisions` JSON (oldest first, stable `id`s), and put `data-lavish-revision="<id>"` on each block you actually edited or added. Lavish reads them and never restyles the page, so the saved file looks the same opened directly.'
|
|
526
|
+
},
|
|
518
527
|
theme_usage: [
|
|
519
528
|
'Default to `<html data-theme="luxury">` - it matches the Lavish look. Pick a different theme from the list below only when the user asked for one or the content clearly calls for it.',
|
|
520
529
|
'Set a nested section theme with `<section data-theme="night">`.',
|
|
@@ -4703,6 +4712,182 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
|
4703
4712
|
import chokidar from "chokidar";
|
|
4704
4713
|
import express from "express";
|
|
4705
4714
|
|
|
4715
|
+
// src/artifact-revisions.js
|
|
4716
|
+
var artifact_revisions_exports = {};
|
|
4717
|
+
__export(artifact_revisions_exports, {
|
|
4718
|
+
collectRevisionMarks: () => collectRevisionMarks,
|
|
4719
|
+
exactRevisionText: () => exactRevisionText,
|
|
4720
|
+
isAddressableRevisionId: () => isAddressableRevisionId,
|
|
4721
|
+
isUniqueElementId: () => isUniqueElementId,
|
|
4722
|
+
normalizeRevisionEntry: () => normalizeRevisionEntry,
|
|
4723
|
+
parseRevisionRegistry: () => parseRevisionRegistry,
|
|
4724
|
+
readArtifactRevisions: () => readArtifactRevisions,
|
|
4725
|
+
revisionLimits: () => revisionLimits,
|
|
4726
|
+
revisionPalette: () => revisionPalette,
|
|
4727
|
+
revisionPresentationForIndex: () => revisionPresentationForIndex,
|
|
4728
|
+
revisionSelectorFor: () => revisionSelectorFor,
|
|
4729
|
+
truncateRevisionText: () => truncateRevisionText
|
|
4730
|
+
});
|
|
4731
|
+
function revisionPalette() {
|
|
4732
|
+
return [
|
|
4733
|
+
{ hex: "#0072b2", borderStyle: "solid", pattern: "none" },
|
|
4734
|
+
{ hex: "#d55e00", borderStyle: "dashed", pattern: "diagonal" },
|
|
4735
|
+
{ hex: "#009e73", borderStyle: "dotted", pattern: "none" },
|
|
4736
|
+
{ hex: "#cc79a7", borderStyle: "double", pattern: "dots" },
|
|
4737
|
+
{ hex: "#56b4e9", borderStyle: "dashed", pattern: "none" },
|
|
4738
|
+
{ hex: "#e69f00", borderStyle: "solid", pattern: "diagonal" }
|
|
4739
|
+
];
|
|
4740
|
+
}
|
|
4741
|
+
function revisionPresentationForIndex(index) {
|
|
4742
|
+
const palette = revisionPalette();
|
|
4743
|
+
const position = Number.isInteger(index) && index >= 0 ? index % palette.length : 0;
|
|
4744
|
+
return palette[position];
|
|
4745
|
+
}
|
|
4746
|
+
function revisionLimits() {
|
|
4747
|
+
return {
|
|
4748
|
+
// One row per palette entry, derived rather than written down: a cap larger
|
|
4749
|
+
// than the palette would hand the seventh revision the first one's colour
|
|
4750
|
+
// *and* its border and pattern, which is precisely the case the palette
|
|
4751
|
+
// exists to prevent.
|
|
4752
|
+
entries: revisionPalette().length,
|
|
4753
|
+
rawEntries: 256,
|
|
4754
|
+
marks: 200,
|
|
4755
|
+
rawMarks: 2e3,
|
|
4756
|
+
registryBytes: 64 * 1024,
|
|
4757
|
+
id: 60,
|
|
4758
|
+
label: 80,
|
|
4759
|
+
summary: 400,
|
|
4760
|
+
timestamp: 40,
|
|
4761
|
+
excerpt: 120,
|
|
4762
|
+
selector: 400,
|
|
4763
|
+
selectorDepth: 32
|
|
4764
|
+
};
|
|
4765
|
+
}
|
|
4766
|
+
function truncateRevisionText(value, max) {
|
|
4767
|
+
if (typeof value !== "string" && typeof value !== "number") return "";
|
|
4768
|
+
const text = String(value).trim();
|
|
4769
|
+
const limit = Number.isInteger(max) && max > 0 ? max : 0;
|
|
4770
|
+
if (!limit || text.length <= limit) return text;
|
|
4771
|
+
return text.slice(0, limit);
|
|
4772
|
+
}
|
|
4773
|
+
function isAddressableRevisionId(id) {
|
|
4774
|
+
const text = typeof id === "string" || typeof id === "number" ? String(id) : "";
|
|
4775
|
+
return text.length > 0 && text === text.trim() && !/\s/.test(text);
|
|
4776
|
+
}
|
|
4777
|
+
function exactRevisionText(value, max) {
|
|
4778
|
+
const text = typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
|
|
4779
|
+
return text.length > 0 && text.length <= max ? text : "";
|
|
4780
|
+
}
|
|
4781
|
+
function normalizeRevisionEntry(entry, index) {
|
|
4782
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
4783
|
+
const limits = revisionLimits();
|
|
4784
|
+
const id = exactRevisionText(entry.id, limits.id);
|
|
4785
|
+
if (!isAddressableRevisionId(id)) return null;
|
|
4786
|
+
return {
|
|
4787
|
+
id,
|
|
4788
|
+
index: Number.isInteger(index) && index >= 0 ? index : 0,
|
|
4789
|
+
label: truncateRevisionText(entry.label, limits.label) || id,
|
|
4790
|
+
timestamp: truncateRevisionText(entry.timestamp, limits.timestamp),
|
|
4791
|
+
summary: truncateRevisionText(entry.summary, limits.summary),
|
|
4792
|
+
mark_count: 0
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
function parseRevisionRegistry(doc) {
|
|
4796
|
+
const script = doc && doc.querySelector ? doc.querySelector("script[data-lavish-revisions]") : null;
|
|
4797
|
+
if (!script) return [];
|
|
4798
|
+
const limits = revisionLimits();
|
|
4799
|
+
const text = String(script.textContent || "").trim();
|
|
4800
|
+
if (!text || text.length > limits.registryBytes) return [];
|
|
4801
|
+
let parsed;
|
|
4802
|
+
try {
|
|
4803
|
+
parsed = JSON.parse(text);
|
|
4804
|
+
} catch {
|
|
4805
|
+
return [];
|
|
4806
|
+
}
|
|
4807
|
+
if (!Array.isArray(parsed)) return [];
|
|
4808
|
+
const revisions = [];
|
|
4809
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4810
|
+
let examined = 0;
|
|
4811
|
+
for (const raw of parsed) {
|
|
4812
|
+
if (examined >= limits.rawEntries || revisions.length >= limits.entries) break;
|
|
4813
|
+
examined += 1;
|
|
4814
|
+
const entry = normalizeRevisionEntry(raw, revisions.length);
|
|
4815
|
+
if (!entry || seen.has(entry.id)) continue;
|
|
4816
|
+
seen.add(entry.id);
|
|
4817
|
+
revisions.push(entry);
|
|
4818
|
+
}
|
|
4819
|
+
return revisions;
|
|
4820
|
+
}
|
|
4821
|
+
function isUniqueElementId(doc, id) {
|
|
4822
|
+
if (!doc || typeof doc.querySelectorAll !== "function") return false;
|
|
4823
|
+
try {
|
|
4824
|
+
const found = doc.querySelectorAll(`#${id}`);
|
|
4825
|
+
return Boolean(found) && found.length === 1;
|
|
4826
|
+
} catch {
|
|
4827
|
+
return false;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
function revisionSelectorFor(element, doc) {
|
|
4831
|
+
const limits = revisionLimits();
|
|
4832
|
+
const parts = [];
|
|
4833
|
+
let node = element;
|
|
4834
|
+
let depth = 0;
|
|
4835
|
+
while (node && node.nodeType === 1) {
|
|
4836
|
+
if (depth >= limits.selectorDepth) return "";
|
|
4837
|
+
const tag = String(node.tagName || "").toLowerCase();
|
|
4838
|
+
if (!tag) return "";
|
|
4839
|
+
const id = node.getAttribute ? String(node.getAttribute("id") || "").trim() : "";
|
|
4840
|
+
if (id && /^[A-Za-z][-\w]*$/.test(id) && isUniqueElementId(doc, id)) {
|
|
4841
|
+
parts.unshift(`#${id}`);
|
|
4842
|
+
return parts.join(" > ");
|
|
4843
|
+
}
|
|
4844
|
+
const parent2 = node.parentElement;
|
|
4845
|
+
let part = tag;
|
|
4846
|
+
if (parent2 && parent2.children) {
|
|
4847
|
+
const siblings = [];
|
|
4848
|
+
for (const child of parent2.children) {
|
|
4849
|
+
if (String(child.tagName || "").toLowerCase() === tag) siblings.push(child);
|
|
4850
|
+
}
|
|
4851
|
+
if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(node) + 1})`;
|
|
4852
|
+
}
|
|
4853
|
+
parts.unshift(part);
|
|
4854
|
+
node = parent2;
|
|
4855
|
+
depth += 1;
|
|
4856
|
+
}
|
|
4857
|
+
return parts[0] === "html" ? parts.join(" > ") : "";
|
|
4858
|
+
}
|
|
4859
|
+
function collectRevisionMarks(doc, revisions) {
|
|
4860
|
+
const limits = revisionLimits();
|
|
4861
|
+
const declared = /* @__PURE__ */ new Map();
|
|
4862
|
+
for (const revision of Array.isArray(revisions) ? revisions : []) {
|
|
4863
|
+
if (revision && isAddressableRevisionId(revision.id)) declared.set(revision.id, revision);
|
|
4864
|
+
}
|
|
4865
|
+
const marks = [];
|
|
4866
|
+
const elements = doc && doc.querySelectorAll ? doc.querySelectorAll("[data-lavish-revision]") : [];
|
|
4867
|
+
let examined = 0;
|
|
4868
|
+
for (const element of elements) {
|
|
4869
|
+
if (examined >= limits.rawMarks || marks.length >= limits.marks) break;
|
|
4870
|
+
examined += 1;
|
|
4871
|
+
const id = element.getAttribute ? exactRevisionText(element.getAttribute("data-lavish-revision"), limits.id) : "";
|
|
4872
|
+
if (!declared.has(id)) continue;
|
|
4873
|
+
const selector = exactRevisionText(revisionSelectorFor(element, doc), limits.selector);
|
|
4874
|
+
if (!selector) continue;
|
|
4875
|
+
declared.get(id).mark_count += 1;
|
|
4876
|
+
marks.push({
|
|
4877
|
+
revision_id: id,
|
|
4878
|
+
selector,
|
|
4879
|
+
tag: String(element.tagName || "").toLowerCase(),
|
|
4880
|
+
excerpt: truncateRevisionText(String(element.textContent || "").replace(/\s+/g, " "), limits.excerpt)
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
return marks;
|
|
4884
|
+
}
|
|
4885
|
+
function readArtifactRevisions(doc) {
|
|
4886
|
+
const revisions = parseRevisionRegistry(doc);
|
|
4887
|
+
const marks = collectRevisionMarks(doc, revisions);
|
|
4888
|
+
return { revisions, marks };
|
|
4889
|
+
}
|
|
4890
|
+
|
|
4706
4891
|
// src/mermaid-node.js
|
|
4707
4892
|
var mermaid_node_exports = {};
|
|
4708
4893
|
__export(mermaid_node_exports, {
|
|
@@ -6840,6 +7025,19 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
6840
7025
|
}
|
|
6841
7026
|
const mermaidObserver = new MutationObserver(() => scheduleMermaidEnhance());
|
|
6842
7027
|
mermaidObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
7028
|
+
function reportArtifactRevisions() {
|
|
7029
|
+
let payload = { revisions: [], marks: [] };
|
|
7030
|
+
try {
|
|
7031
|
+
payload = readArtifactRevisions(document);
|
|
7032
|
+
} catch {
|
|
7033
|
+
}
|
|
7034
|
+
postArtifactMessage("lavish:revisions", payload);
|
|
7035
|
+
}
|
|
7036
|
+
if (document.readyState === "loading") {
|
|
7037
|
+
document.addEventListener("DOMContentLoaded", reportArtifactRevisions, { once: true });
|
|
7038
|
+
} else {
|
|
7039
|
+
reportArtifactRevisions();
|
|
7040
|
+
}
|
|
6843
7041
|
}
|
|
6844
7042
|
|
|
6845
7043
|
// src/layout-warnings.js
|
|
@@ -11492,7 +11690,12 @@ function createChromeHtml(session, {
|
|
|
11492
11690
|
modeToggleHotkeyKey: MODE_TOGGLE_HOTKEY_KEY,
|
|
11493
11691
|
attachmentMaxBytes,
|
|
11494
11692
|
attachmentMaxCount,
|
|
11495
|
-
attachmentAcceptedMime: acceptedMime
|
|
11693
|
+
attachmentAcceptedMime: acceptedMime,
|
|
11694
|
+
// The legend's swatches are server-owned, not artifact-owned. The chrome
|
|
11695
|
+
// derives every swatch from this palette by registry position, so an
|
|
11696
|
+
// artifact cannot hand all its revisions the same colour and pattern and
|
|
11697
|
+
// collapse the one signal that tells the rounds apart.
|
|
11698
|
+
revisionPalette: revisionPalette()
|
|
11496
11699
|
});
|
|
11497
11700
|
const { head: pathHead, tail: pathTail } = displayPathParts(session.file);
|
|
11498
11701
|
const bodyClass = layoutGateEnabled ? "lavish layout-gate-active" : "lavish";
|
|
@@ -11509,7 +11712,7 @@ ${faviconTag}
|
|
|
11509
11712
|
<link rel="stylesheet" href="/chrome.css">
|
|
11510
11713
|
</head>
|
|
11511
11714
|
<body class="${bodyClass}">
|
|
11512
|
-
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="spacer" aria-hidden="true"></div><div class="warnings-wrap" id="warningsWrap" hidden><button class="warnings-button" id="warningsButton" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="warningsDrawer">${chromeIcons.warning}<span class="warnings-count" id="warningsCount">0</span></button><div class="menu warnings-drawer" id="warningsDrawer" role="dialog" aria-labelledby="warningsTitle" aria-describedby="warningsSummary" hidden><div class="warnings-head"><h2 class="warnings-title" id="warningsTitle">Layout issues</h2><p class="warnings-summary" id="warningsSummary"></p></div><div class="warnings-toolbar"><label class="warnings-selectall"><input type="checkbox" id="warningsSelectAll"><span>Select all</span></label><span class="warnings-selected" id="warningsSelected" role="status" aria-live="polite"></span></div><div class="warnings-list" id="warningsList"></div><div class="warnings-foot"><p class="warnings-note">Queueing sends a repair request with your next feedback. An issue is marked resolved only after a newer artifact load and a complete check at the same viewport no longer finds it.</p><button class="button" id="warningsQueueButton" type="button" disabled>Queue selected fixes</button></div></div></div><button class="annotate-switch" id="annotation" type="button" aria-pressed="true" title="${escapeHtml2(modeToggleHint)}"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml2(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml2(pathHead)}</span><span class="path-tail">${escapeHtml2(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
11715
|
+
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="spacer" aria-hidden="true"></div><div class="warnings-wrap" id="warningsWrap" hidden><button class="warnings-button" id="warningsButton" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="warningsDrawer">${chromeIcons.warning}<span class="warnings-count" id="warningsCount">0</span></button><div class="menu warnings-drawer" id="warningsDrawer" role="dialog" aria-labelledby="warningsTitle" aria-describedby="warningsSummary" hidden><div class="warnings-head"><h2 class="warnings-title" id="warningsTitle">Layout issues</h2><p class="warnings-summary" id="warningsSummary"></p></div><div class="warnings-toolbar"><label class="warnings-selectall"><input type="checkbox" id="warningsSelectAll"><span>Select all</span></label><span class="warnings-selected" id="warningsSelected" role="status" aria-live="polite"></span></div><div class="warnings-list" id="warningsList"></div><div class="warnings-foot"><p class="warnings-note">Queueing sends a repair request with your next feedback. An issue is marked resolved only after a newer artifact load and a complete check at the same viewport no longer finds it.</p><button class="button" id="warningsQueueButton" type="button" disabled>Queue selected fixes</button></div></div></div><div class="revisions-wrap" id="revisionsWrap" hidden><button class="revisions-button" id="revisionsButton" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="revisionsDrawer"><span class="revisions-button-text">Revisions</span><span class="revisions-count" id="revisionsCount">0</span></button><div class="menu revisions-drawer" id="revisionsDrawer" role="dialog" aria-labelledby="revisionsTitle" aria-describedby="revisionsSummary" hidden><div class="revisions-head"><h2 class="revisions-title" id="revisionsTitle">Revisions</h2><p class="revisions-summary" id="revisionsSummary"></p></div><div class="revisions-list" id="revisionsList"></div><div class="revisions-foot"><p class="revisions-note">The agent declares these in the artifact itself. Reveal flashes the next block it marked for that revision; nothing about the page is restyled, so the saved file still looks the way it does here.</p></div></div></div><button class="annotate-switch" id="annotation" type="button" aria-pressed="true" title="${escapeHtml2(modeToggleHint)}"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml2(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml2(pathHead)}</span><span class="path-tail">${escapeHtml2(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
11513
11716
|
<div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe></div><div class="panel-scrim" id="panelScrim"></div><aside class="panel" id="panel"><div class="panel-head" id="panelHead"><span class="panel-handle" aria-hidden="true"></span><div class="panel-head-row"><h2>Conversation</h2><span class="panel-summary" id="panelSummary" role="status" aria-live="polite"></span><button class="panel-toggle" id="panelToggle" type="button" aria-expanded="false" aria-controls="panel" aria-label="Show conversation">${chromeIcons.chevronUp}</button></div></div><div class="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="chat chat-queued" id="queuedLog"></div></div><div class="composer" id="chatComposer"><div class="presence-banner handoff-banner" id="handoffBanner" hidden><span>This review is open in another Lavish tab.</span><button class="handoff-takeover" id="handoffTakeover" type="button">Take over here</button></div><div class="presence-banner handoff-banner" id="outdatedBanner" hidden><span id="outdatedText">The Lavish server this page was connected to is no longer running. Reloading will work once it is running again.</span><span class="outdated-actions"><button class="handoff-takeover" id="outdatedReload" type="button">Check and reload</button><button class="handoff-takeover" id="outdatedDismiss" type="button">Dismiss</button></span></div><div class="presence-banner" id="presenceBanner" hidden>Your agent is not listening. If this persists, ask your agent to poll for updates from Lavish.</div><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="chat-attachments" id="chatAttachments"></div><div class="chat-attachment-toolbar"><button class="chat-attach" id="chatAttach" type="button">Attach images</button><input id="chatAttachInput" type="file" accept="${escapeHtml2(acceptedMime.join(","))}" multiple hidden><span class="chat-attachment-notice" id="chatAttachmentNotice" role="status"></span></div><div class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</div><div class="actions" id="sendActions"><button class="button button-danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send & End</span></button><button class="button" id="send">Send to Agent</button></div></div></aside></div>
|
|
11514
11717
|
<div class="share-overlay" id="shareDialog" role="dialog" aria-modal="true" aria-labelledby="shareTitleText" hidden><form class="share-card" id="shareForm"><div class="share-head"><div><div class="share-kicker">Publish to <a class="share-link" href="https://ht-ml.app" target="_blank" rel="noopener noreferrer">ht-ml.app</a></div><h2 id="shareTitleText">Publish artifact</h2></div><button class="share-close" id="shareClose" type="button" aria-label="Close publish dialog"><svg width="14" height="14" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button></div><p class="share-note">ht-ml.app is a separate, third-party hosting service, not part of Lavish. Publishing sends this artifact to its servers.</p><p class="share-copy">This uploads this artifact to ht-ml.app with local assets inlined. Without a password, the page is PUBLIC and anyone with the link can open it. With a password, the page is PRIVATE and viewers must supply the password to view.</p><p class="share-note">Do not publish secrets. The Lavish annotation SDK is not included.</p><div class="share-grid"><label class="share-check"><input id="shareGenerate" type="checkbox"><span>Generate a password (makes this page private)</span></label><label>Password (optional)<input id="sharePassword" name="password" type="password" autocomplete="new-password" placeholder="Leave blank for a public page"></label></div><div class="share-status" id="shareStatus" role="status"></div><div class="share-result" id="shareResult" hidden><label id="shareUrlResult">Share URL<div class="share-copy-row"><input id="shareUrl" readonly><button class="share-copy-btn" id="copyShareUrl" type="button">Copy URL</button></div></label><label id="sharePasswordResult" hidden>Password (shared secret)<div class="share-copy-row"><input id="sharePasswordOut" readonly><button class="share-copy-btn" id="copySharePassword" type="button">Copy password</button></div></label><label id="shareSiteIdResult" hidden>Site ID<div class="share-copy-row"><input id="shareSiteId" readonly><button class="share-copy-btn" id="copyShareSiteId" type="button">Copy site ID</button></div></label><label id="shareUpdateKeyResult">Update key (secret)<div class="share-copy-row"><input id="shareUpdateKey" readonly><button class="share-copy-btn" id="copyUpdateKey" type="button">Copy key</button></div></label><p class="share-note" id="shareUpdateKeyNote">Keep the update key private. ht-ml.app returns it once and it is the only way to update this page later; the service has no delete. Republish this page's HTML with <code>lavish-axi share <file> --site <site id> --update-key <key></code>, and add <code>--private</code> to also lock it behind a new generated password.</p></div><div class="share-actions"><button class="share-cancel" id="shareCancel" type="button">Cancel</button><button class="button" id="sharePublish" type="submit">Publish</button></div></form></div>
|
|
11515
11718
|
<div class="ended-overlay layout-gate-overlay" id="layoutGateOverlay"${layoutGateHidden}><div class="ended-card"><div class="ended-title" id="layoutGateTitle">Checking layout.<br>One moment.</div><p class="ended-copy" id="layoutGateCopy">Lavish is waiting for fonts and final geometry before revealing this artifact.</p><button class="button ended-action" id="layoutGateAction" type="button">Show anyway</button><button class="button ended-action layout-gate-bypass" id="layoutGateBypass" type="button" hidden>Show anyway</button></div></div>
|
|
@@ -11552,6 +11755,7 @@ function serializeModuleHelpers(module) {
|
|
|
11552
11755
|
function createSdkJs(key, artifactRevision = 0, artifactLoadToken = "", { maxAttachmentCount, maxAttachmentBytes, acceptedImageMime = ACCEPTED_IMAGE_MIME } = {}) {
|
|
11553
11756
|
const mermaidHelperSource = serializeModuleHelpers(mermaid_node_exports);
|
|
11554
11757
|
const tableHelperSource = serializeModuleHelpers(table_cell_exports);
|
|
11758
|
+
const revisionHelperSource = serializeModuleHelpers(artifact_revisions_exports);
|
|
11555
11759
|
const revisionNumber = Number(artifactRevision);
|
|
11556
11760
|
const revision = Number.isFinite(revisionNumber) && revisionNumber >= 0 ? Math.trunc(revisionNumber) : 0;
|
|
11557
11761
|
const loadToken = String(artifactLoadToken || "").slice(0, 200);
|
|
@@ -11583,6 +11787,7 @@ const deriveAttachmentNoticeState=${deriveAttachmentNoticeState.toString()};
|
|
|
11583
11787
|
${mermaidHelperSource.declarations}
|
|
11584
11788
|
const mermaidHelpers={ ${mermaidHelperSource.names.join(", ")} };
|
|
11585
11789
|
${tableHelperSource.declarations}
|
|
11790
|
+
${revisionHelperSource.declarations}
|
|
11586
11791
|
(${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl, mermaidHelpers, artifactRevision, artifactLoadToken, key, ${JSON.stringify(sdkOptions)});
|
|
11587
11792
|
})();`;
|
|
11588
11793
|
}
|
|
@@ -11790,7 +11995,7 @@ var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_J
|
|
|
11790
11995
|
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
11791
11996
|
var POLL_STATE_HEADER = "lavish-poll-state";
|
|
11792
11997
|
var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
|
|
11793
|
-
var VERSION = "0.1.
|
|
11998
|
+
var VERSION = "0.1.77";
|
|
11794
11999
|
function detectInvokingAgent(env = process.env) {
|
|
11795
12000
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
11796
12001
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "lavish-axi",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.77",
|
|
5
5
|
"description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kun Chen",
|
package/skills/lavish/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lavish
|
|
3
|
-
description: Turn complex or visual agent responses into rich, reviewable HTML artifacts the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.
|
|
3
|
+
description: Turn complex or visual agent responses into rich, reviewable HTML artifacts (HTML files) the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: Kun Chen (kunchenguid)
|
|
@@ -29,5 +29,5 @@ If lavish-axi output shows a follow-up command starting with `lavish-axi`, run i
|
|
|
29
29
|
|
|
30
30
|
$ARGUMENTS
|
|
31
31
|
|
|
32
|
-
If the request above is non-empty, the user invoked `/lavish` explicitly - fetch the current CLI guidance, then build that artifact.
|
|
32
|
+
If the request above is non-empty, the user invoked `/lavish` explicitly - fetch the current CLI guidance, then build that artifact as an HTML file.
|
|
33
33
|
If it is empty, infer what to visualize from the conversation.
|