lavish-axi 0.1.35 → 0.1.36
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 +6 -2
- package/dist/chrome-client.js +23 -2
- package/dist/cli.mjs +241 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,10 +31,10 @@ HTML is the new markdown. Lavish is the new editor for your HTML artifacts.
|
|
|
31
31
|
Agents are good at producing rich HTML artifacts, but the human-agent collaboration loop on such artifacts is lacking and falls back into screenshots and long responses for “tell me what to change.”
|
|
32
32
|
That loses the thing HTML is best at: interactivity.
|
|
33
33
|
|
|
34
|
-
Lavish Editor opens agent-generated HTML files in a local browser, lets you pinpoint elements
|
|
34
|
+
Lavish Editor opens agent-generated HTML files in a local browser, lets you pinpoint elements, selected text, or Mermaid diagram nodes and send feedback to the agent to address.
|
|
35
35
|
|
|
36
36
|
- **Local-first** - Review local HTML artifacts with a local CLI and no cloud dependency in the core feedback loop; hosted sharing through third-party ht-ml.app is explicit and opt-in.
|
|
37
|
-
- **Human-AI collaboration** - Annotate elements, selected text ranges, and send messages to the agent without leaving Lavish Editor.
|
|
37
|
+
- **Human-AI collaboration** - Annotate elements, selected text ranges, and Mermaid diagram nodes, and send messages to the agent without leaving Lavish Editor.
|
|
38
38
|
- **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as product or technical plans, design explorations and more out of the box.
|
|
39
39
|
|
|
40
40
|
Lavish Editor is an [AXI](https://axi.md), which means -
|
|
@@ -54,6 +54,8 @@ npx skills add kunchenguid/lavish-axi --skill lavish
|
|
|
54
54
|
That is the entire setup - no npm install needed.
|
|
55
55
|
The skill teaches your agent to run Lavish through `npx -y lavish-axi`, so the CLI comes along on demand.
|
|
56
56
|
Its frontmatter also includes Hermes Agent metadata, so Hermes-compatible harnesses can categorize and surface it as a first-class productivity skill.
|
|
57
|
+
This installs the public `lavish` skill.
|
|
58
|
+
The repository also contains an internal `lavish-design` brand skill for maintainers; default `npx skills add ... --list` and skills.sh discovery hide it unless `INSTALL_INTERNAL_SKILLS=1` is set.
|
|
57
59
|
|
|
58
60
|
Then, in agents that expose skills as slash commands (Claude Code, for example), invoke it directly:
|
|
59
61
|
|
|
@@ -158,12 +160,14 @@ pnpm link
|
|
|
158
160
|
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.
|
|
159
161
|
- **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
|
|
160
162
|
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.
|
|
163
|
+
Cmd+I or Ctrl+I toggles between annotate and explore mode from either the browser chrome or the artifact iframe, including while focus is in a textarea or control.
|
|
161
164
|
- **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
165
|
- **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
166
|
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
167
|
Agent-initiated ends keep reopening normally, same as before.
|
|
165
168
|
`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.
|
|
166
169
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
170
|
+
- **Mermaid diagrams** - Rendered Mermaid diagrams become pannable and zoomable while you explore (drag to pan, scroll to zoom) and freeze when you turn on annotation so a click lands on a single node. Clicking a node annotates the whole node and sends the agent its diagram id, node id, and rendered label instead of just a CSS selector. Lavish only enhances the live render, so the saved HTML still opens identically anywhere.
|
|
167
171
|
- **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.
|
|
168
172
|
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
169
173
|
- **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
|
package/dist/chrome-client.js
CHANGED
|
@@ -7,6 +7,12 @@ const filePath = String(sessionData.file || "");
|
|
|
7
7
|
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
8
8
|
const internalQueueKeyField = "_lavishQueueKey";
|
|
9
9
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
10
|
+
const MODE_TOGGLE_HOTKEY_KEY = String(sessionData.modeToggleHotkeyKey || "").toLowerCase();
|
|
11
|
+
|
|
12
|
+
function isModeToggleHotkeyEvent(event) {
|
|
13
|
+
if (event.shiftKey || event.altKey) return false;
|
|
14
|
+
return Boolean(event.metaKey || event.ctrlKey) && String(event.key || "").toLowerCase() === MODE_TOGGLE_HOTKEY_KEY;
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
|
|
12
18
|
const annotationPills = /** @type {HTMLDivElement} */ (document.getElementById("annotationPills"));
|
|
@@ -701,15 +707,19 @@ window.addEventListener("message", (event) => {
|
|
|
701
707
|
}
|
|
702
708
|
if (msg.type === "lavish:sendQueuedPrompts") sendQueued();
|
|
703
709
|
if (msg.type === "lavish:endSession") endSession();
|
|
710
|
+
if (msg.type === "lavish:toggleAnnotationMode") toggleAnnotationMode();
|
|
704
711
|
});
|
|
705
712
|
|
|
706
713
|
loadFrame();
|
|
707
714
|
|
|
708
|
-
|
|
715
|
+
function toggleAnnotationMode() {
|
|
716
|
+
if (ended) return;
|
|
709
717
|
annotation = !annotation;
|
|
710
718
|
annotationSwitch.setAttribute("aria-pressed", String(annotation));
|
|
711
719
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
|
|
712
|
-
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
annotationSwitch.onclick = toggleAnnotationMode;
|
|
713
723
|
|
|
714
724
|
sendButton.onclick = () => sendQueued(false);
|
|
715
725
|
sendFromMenuButton.onclick = () => sendQueued(false);
|
|
@@ -754,6 +764,17 @@ document.addEventListener("keydown", (event) => {
|
|
|
754
764
|
}
|
|
755
765
|
}
|
|
756
766
|
});
|
|
767
|
+
// Capture phase so the mode hotkey fires no matter where focus is in the chrome - including
|
|
768
|
+
// mid-keystroke in chatInput or an annotation-card textarea - without disturbing normal typing.
|
|
769
|
+
document.addEventListener(
|
|
770
|
+
"keydown",
|
|
771
|
+
(event) => {
|
|
772
|
+
if (!isModeToggleHotkeyEvent(event)) return;
|
|
773
|
+
event.preventDefault();
|
|
774
|
+
toggleAnnotationMode();
|
|
775
|
+
},
|
|
776
|
+
true,
|
|
777
|
+
);
|
|
757
778
|
frame.addEventListener("load", () => {
|
|
758
779
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation && !ended });
|
|
759
780
|
// Replay the pre-reload scroll position so hot reloads don't jump the artifact to the top.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
// src/cli.js
|
|
4
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -4188,7 +4193,67 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
4188
4193
|
import chokidar from "chokidar";
|
|
4189
4194
|
import express from "express";
|
|
4190
4195
|
|
|
4196
|
+
// src/mermaid-node.js
|
|
4197
|
+
var mermaid_node_exports = {};
|
|
4198
|
+
__export(mermaid_node_exports, {
|
|
4199
|
+
isMermaidSvg: () => isMermaidSvg,
|
|
4200
|
+
mermaidNodeElement: () => mermaidNodeElement,
|
|
4201
|
+
mermaidNodeFrom: () => mermaidNodeFrom,
|
|
4202
|
+
normalizeMermaidNodeTarget: () => normalizeMermaidNodeTarget,
|
|
4203
|
+
readNodeLabel: () => readNodeLabel
|
|
4204
|
+
});
|
|
4205
|
+
function isMermaidSvg(svg) {
|
|
4206
|
+
if (!svg) return false;
|
|
4207
|
+
const id = svg.id || "";
|
|
4208
|
+
if (id.startsWith("mermaid-") || id.startsWith("mermaid_")) return true;
|
|
4209
|
+
if (svg.getAttribute?.("aria-roledescription")) return true;
|
|
4210
|
+
return !!(svg.closest && svg.closest(".mermaid, [data-lavish-mermaid]"));
|
|
4211
|
+
}
|
|
4212
|
+
function readNodeLabel(labelEl) {
|
|
4213
|
+
if (!labelEl) return "";
|
|
4214
|
+
let source = labelEl;
|
|
4215
|
+
if (labelEl.querySelector?.("br") && labelEl.cloneNode) {
|
|
4216
|
+
source = labelEl.cloneNode(true);
|
|
4217
|
+
for (const br of source.querySelectorAll("br")) br.replaceWith(document.createTextNode(" "));
|
|
4218
|
+
}
|
|
4219
|
+
return (source.textContent || "").trim().replace(/\s+/g, " ").slice(0, 120);
|
|
4220
|
+
}
|
|
4221
|
+
function mermaidNodeElement(el) {
|
|
4222
|
+
if (!el || !el.closest) return null;
|
|
4223
|
+
const node = el.closest("g.node, g.nodes > g");
|
|
4224
|
+
if (!node) return null;
|
|
4225
|
+
const svg = node.closest("svg");
|
|
4226
|
+
return svg && isMermaidSvg(svg) ? node : null;
|
|
4227
|
+
}
|
|
4228
|
+
function mermaidNodeFrom(el, selector) {
|
|
4229
|
+
const node = mermaidNodeElement(el);
|
|
4230
|
+
if (!node) return null;
|
|
4231
|
+
const svg = node.closest("svg");
|
|
4232
|
+
const labelEl = node.querySelector(".nodeLabel, .label, foreignObject span, text");
|
|
4233
|
+
return {
|
|
4234
|
+
type: "mermaid-node",
|
|
4235
|
+
diagramId: svg.id || "",
|
|
4236
|
+
nodeId: node.id || "",
|
|
4237
|
+
label: readNodeLabel(labelEl),
|
|
4238
|
+
selector: typeof selector === "function" ? selector(node) : ""
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function normalizeMermaidNodeTarget(target) {
|
|
4242
|
+
return {
|
|
4243
|
+
type: "mermaid-node",
|
|
4244
|
+
diagramId: String(target.diagramId || ""),
|
|
4245
|
+
nodeId: String(target.nodeId || ""),
|
|
4246
|
+
label: String(target.label || ""),
|
|
4247
|
+
selector: String(target.selector || "")
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4191
4251
|
// src/artifact-sdk.js
|
|
4252
|
+
var MODE_TOGGLE_HOTKEY_KEY = "i";
|
|
4253
|
+
function isModeToggleHotkeyEvent(event) {
|
|
4254
|
+
if (event.shiftKey || event.altKey) return false;
|
|
4255
|
+
return Boolean(event.metaKey || event.ctrlKey) && String(event.key || "").toLowerCase() === MODE_TOGGLE_HOTKEY_KEY;
|
|
4256
|
+
}
|
|
4192
4257
|
function deriveLavishQueueKey(element, options = {}) {
|
|
4193
4258
|
function stringValue(value) {
|
|
4194
4259
|
return value === null || value === void 0 ? "" : String(value);
|
|
@@ -4328,7 +4393,8 @@ function resolveVisibleSpillCandidates(spillCandidates, { epsilon = 1 } = {}) {
|
|
|
4328
4393
|
)
|
|
4329
4394
|
);
|
|
4330
4395
|
}
|
|
4331
|
-
function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInteractiveControl) {
|
|
4396
|
+
function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInteractiveControl, mermaid = mermaid_node_exports) {
|
|
4397
|
+
const { isMermaidSvg: isMermaidSvg2, mermaidNodeFrom: mermaidNodeFrom2, mermaidNodeElement: mermaidNodeElement2 } = mermaid;
|
|
4332
4398
|
let annotationMode = true;
|
|
4333
4399
|
let hovered = null;
|
|
4334
4400
|
let selected = null;
|
|
@@ -4340,6 +4406,12 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4340
4406
|
if (!ids.has(el)) ids.set(el, String(++counter));
|
|
4341
4407
|
return ids.get(el);
|
|
4342
4408
|
}
|
|
4409
|
+
function escapeAnnotationText(value) {
|
|
4410
|
+
return String(value).replace(
|
|
4411
|
+
/[&<>"']/g,
|
|
4412
|
+
(char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4343
4415
|
function selector(el) {
|
|
4344
4416
|
if (!el || !el.tagName) return "";
|
|
4345
4417
|
const parts = [];
|
|
@@ -4362,12 +4434,138 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4362
4434
|
return parts.join(" > ");
|
|
4363
4435
|
}
|
|
4364
4436
|
function context(el) {
|
|
4365
|
-
|
|
4437
|
+
const base = {
|
|
4366
4438
|
uid: uid(el),
|
|
4367
4439
|
selector: selector(el),
|
|
4368
4440
|
tag: (el.tagName || "").toLowerCase(),
|
|
4369
4441
|
text: (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 240)
|
|
4370
4442
|
};
|
|
4443
|
+
const mermaidNode = mermaidNodeFrom2(el, selector);
|
|
4444
|
+
if (mermaidNode) {
|
|
4445
|
+
base.tag = "mermaid-node";
|
|
4446
|
+
base.text = mermaidNode.label || base.text;
|
|
4447
|
+
base.target = mermaidNode;
|
|
4448
|
+
}
|
|
4449
|
+
return base;
|
|
4450
|
+
}
|
|
4451
|
+
function annotationTargetEl(el) {
|
|
4452
|
+
return mermaidNodeElement2(el) || el;
|
|
4453
|
+
}
|
|
4454
|
+
const mermaidViewports = /* @__PURE__ */ new WeakMap();
|
|
4455
|
+
function findMermaidSvgs() {
|
|
4456
|
+
const svgs = /* @__PURE__ */ new Set();
|
|
4457
|
+
for (const svg of document.querySelectorAll("svg")) {
|
|
4458
|
+
if (isMermaidSvg2(svg)) svgs.add(svg);
|
|
4459
|
+
}
|
|
4460
|
+
return [...svgs];
|
|
4461
|
+
}
|
|
4462
|
+
function createViewport(svg) {
|
|
4463
|
+
const bbox = svg.getBBox ? safeBBox(svg) : null;
|
|
4464
|
+
const initial = readViewBox(svg) || (bbox ? { x: bbox.x, y: bbox.y, w: bbox.width, h: bbox.height } : null);
|
|
4465
|
+
if (!initial) return null;
|
|
4466
|
+
svg.setAttribute("viewBox", `${initial.x} ${initial.y} ${initial.w} ${initial.h}`);
|
|
4467
|
+
const view = { ...initial };
|
|
4468
|
+
let frozen = false;
|
|
4469
|
+
let panning = null;
|
|
4470
|
+
function apply() {
|
|
4471
|
+
svg.setAttribute("viewBox", `${view.x} ${view.y} ${view.w} ${view.h}`);
|
|
4472
|
+
}
|
|
4473
|
+
function reset() {
|
|
4474
|
+
Object.assign(view, initial);
|
|
4475
|
+
apply();
|
|
4476
|
+
}
|
|
4477
|
+
function zoomAt(clientX, clientY, factor) {
|
|
4478
|
+
const rect = svg.getBoundingClientRect();
|
|
4479
|
+
if (!rect.width || !rect.height) return;
|
|
4480
|
+
const px = (clientX - rect.left) / rect.width;
|
|
4481
|
+
const py = (clientY - rect.top) / rect.height;
|
|
4482
|
+
const fx = view.x + view.w * px;
|
|
4483
|
+
const fy = view.y + view.h * py;
|
|
4484
|
+
const next = Math.min(Math.max(view.w * factor, initial.w / 40), initial.w * 8);
|
|
4485
|
+
const scale = next / view.w;
|
|
4486
|
+
view.w = next;
|
|
4487
|
+
view.h *= scale;
|
|
4488
|
+
view.x = fx - (fx - view.x) * scale;
|
|
4489
|
+
view.y = fy - (fy - view.y) * scale;
|
|
4490
|
+
apply();
|
|
4491
|
+
}
|
|
4492
|
+
function onWheel(event) {
|
|
4493
|
+
if (frozen) return;
|
|
4494
|
+
event.preventDefault();
|
|
4495
|
+
zoomAt(event.clientX, event.clientY, event.deltaY > 0 ? 1.15 : 1 / 1.15);
|
|
4496
|
+
}
|
|
4497
|
+
function onPointerDown(event) {
|
|
4498
|
+
if (frozen || event.button !== 0) return;
|
|
4499
|
+
panning = { x: event.clientX, y: event.clientY, vx: view.x, vy: view.y };
|
|
4500
|
+
svg.setPointerCapture?.(event.pointerId);
|
|
4501
|
+
svg.style.cursor = "grabbing";
|
|
4502
|
+
}
|
|
4503
|
+
function onPointerMove(event) {
|
|
4504
|
+
if (!panning) return;
|
|
4505
|
+
const rect = svg.getBoundingClientRect();
|
|
4506
|
+
if (!rect.width || !rect.height) return;
|
|
4507
|
+
view.x = panning.vx - (event.clientX - panning.x) / rect.width * view.w;
|
|
4508
|
+
view.y = panning.vy - (event.clientY - panning.y) / rect.height * view.h;
|
|
4509
|
+
apply();
|
|
4510
|
+
}
|
|
4511
|
+
function onPointerUp(event) {
|
|
4512
|
+
panning = null;
|
|
4513
|
+
svg.releasePointerCapture?.(event.pointerId);
|
|
4514
|
+
svg.style.cursor = frozen ? "" : "grab";
|
|
4515
|
+
}
|
|
4516
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
4517
|
+
svg.addEventListener("pointerdown", onPointerDown);
|
|
4518
|
+
svg.addEventListener("pointermove", onPointerMove);
|
|
4519
|
+
svg.addEventListener("pointerup", onPointerUp);
|
|
4520
|
+
svg.addEventListener("pointercancel", onPointerUp);
|
|
4521
|
+
function setFrozen(next) {
|
|
4522
|
+
frozen = !!next;
|
|
4523
|
+
panning = null;
|
|
4524
|
+
svg.style.cursor = frozen ? "" : "grab";
|
|
4525
|
+
svg.style.touchAction = frozen ? "" : "none";
|
|
4526
|
+
}
|
|
4527
|
+
setFrozen(false);
|
|
4528
|
+
return { reset, setFrozen };
|
|
4529
|
+
}
|
|
4530
|
+
function safeBBox(svg) {
|
|
4531
|
+
try {
|
|
4532
|
+
return svg.getBBox();
|
|
4533
|
+
} catch {
|
|
4534
|
+
return null;
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
function readViewBox(svg) {
|
|
4538
|
+
const raw = svg.getAttribute?.("viewBox");
|
|
4539
|
+
if (!raw) return null;
|
|
4540
|
+
const parts = raw.trim().split(/[\s,]+/).map(Number);
|
|
4541
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return null;
|
|
4542
|
+
return { x: parts[0], y: parts[1], w: parts[2], h: parts[3] };
|
|
4543
|
+
}
|
|
4544
|
+
function enhanceMermaid() {
|
|
4545
|
+
for (const svg of findMermaidSvgs()) {
|
|
4546
|
+
if (mermaidViewports.has(svg)) continue;
|
|
4547
|
+
const viewport = createViewport(svg);
|
|
4548
|
+
if (viewport) {
|
|
4549
|
+
viewport.setFrozen(annotationMode);
|
|
4550
|
+
mermaidViewports.set(svg, viewport);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
let mermaidEnhanceScheduled = false;
|
|
4555
|
+
function scheduleMermaidEnhance() {
|
|
4556
|
+
if (mermaidEnhanceScheduled) return;
|
|
4557
|
+
mermaidEnhanceScheduled = true;
|
|
4558
|
+
const run2 = () => {
|
|
4559
|
+
mermaidEnhanceScheduled = false;
|
|
4560
|
+
enhanceMermaid();
|
|
4561
|
+
};
|
|
4562
|
+
if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(run2);
|
|
4563
|
+
else window.setTimeout(run2, 50);
|
|
4564
|
+
}
|
|
4565
|
+
function setMermaidFrozen(frozen) {
|
|
4566
|
+
for (const svg of findMermaidSvgs()) {
|
|
4567
|
+
mermaidViewports.get(svg)?.setFrozen(frozen);
|
|
4568
|
+
}
|
|
4371
4569
|
}
|
|
4372
4570
|
function closestElement(node) {
|
|
4373
4571
|
if (!node) return document.body;
|
|
@@ -4465,6 +4663,7 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4465
4663
|
}
|
|
4466
4664
|
if (!annotationMode && style) style.remove();
|
|
4467
4665
|
if (!annotationMode) closeCard();
|
|
4666
|
+
setMermaidFrozen(annotationMode);
|
|
4468
4667
|
}
|
|
4469
4668
|
function queuePrompt(prompt, options = {}) {
|
|
4470
4669
|
const originElement = options.element || document.activeElement || document.body;
|
|
@@ -4832,17 +5031,20 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4832
5031
|
const root = ensureShadow();
|
|
4833
5032
|
closeCard();
|
|
4834
5033
|
const c = options.context || context(target);
|
|
5034
|
+
let anchor = target;
|
|
4835
5035
|
if (options.range) {
|
|
4836
5036
|
highlightTextRange(options.range);
|
|
4837
5037
|
} else {
|
|
4838
|
-
|
|
5038
|
+
anchor = annotationTargetEl(target);
|
|
5039
|
+
selected = anchor;
|
|
4839
5040
|
highlightElement(selected);
|
|
4840
5041
|
}
|
|
4841
|
-
const rect = options.range ? options.range.getBoundingClientRect() :
|
|
5042
|
+
const rect = options.range ? options.range.getBoundingClientRect() : anchor.getBoundingClientRect();
|
|
4842
5043
|
const card = document.createElement("div");
|
|
4843
5044
|
card.className = "lavish-annotation-card";
|
|
4844
|
-
const
|
|
4845
|
-
const
|
|
5045
|
+
const nodeLabel = c.tag === "mermaid-node" ? c.target?.label || c.text || "" : "";
|
|
5046
|
+
const heading = c.tag === "text" ? "Annotate text" : c.tag === "mermaid-node" ? "Annotate node" + (nodeLabel ? ": " + escapeAnnotationText(nodeLabel) : "") : "Annotate <" + c.tag + ">";
|
|
5047
|
+
const placeholder = c.tag === "text" ? "Tell the agent what to change about this text..." : c.tag === "mermaid-node" ? "Tell the agent what to change about this diagram node..." : "Tell the agent what to change about this element...";
|
|
4846
5048
|
card.innerHTML = '<div class="lavish-heading">' + heading + '</div><textarea placeholder="' + placeholder + '"></textarea><div class="lavish-hint">Enter to queue · ' + (/Mac|iP(hone|ad|od)/.test(navigator.platform) ? "\u2318" : "Ctrl") + '+Enter to send now</div><div class="lavish-row"><button class="lavish-cancel" type="button">Cancel</button><button class="lavish-send" type="button">Queue</button></div>';
|
|
4847
5049
|
root.appendChild(card);
|
|
4848
5050
|
const left = Math.min(Math.max(12, rect.left), window.innerWidth - card.offsetWidth - 12);
|
|
@@ -4896,6 +5098,15 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4896
5098
|
window.scrollTo(Number(msg.x) || 0, Number(msg.y) || 0);
|
|
4897
5099
|
}
|
|
4898
5100
|
});
|
|
5101
|
+
document.addEventListener(
|
|
5102
|
+
"keydown",
|
|
5103
|
+
(event) => {
|
|
5104
|
+
if (!isModeToggleHotkeyEvent(event)) return;
|
|
5105
|
+
event.preventDefault();
|
|
5106
|
+
parent.postMessage({ type: "lavish:toggleAnnotationMode" }, "*");
|
|
5107
|
+
},
|
|
5108
|
+
true
|
|
5109
|
+
);
|
|
4899
5110
|
let scrollFrame = 0;
|
|
4900
5111
|
window.addEventListener(
|
|
4901
5112
|
"scroll",
|
|
@@ -4913,9 +5124,10 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4913
5124
|
(event) => {
|
|
4914
5125
|
if (!annotationMode || isLavishUi(event.target) || isLavishAction(event.target) || isInteractiveControl(event.target))
|
|
4915
5126
|
return;
|
|
4916
|
-
|
|
5127
|
+
const target = annotationTargetEl(event.target);
|
|
5128
|
+
if (target === selected) return;
|
|
4917
5129
|
if (hovered && hovered !== selected) clearHighlight(hovered);
|
|
4918
|
-
hovered =
|
|
5130
|
+
hovered = target;
|
|
4919
5131
|
highlightElement(hovered);
|
|
4920
5132
|
},
|
|
4921
5133
|
true
|
|
@@ -4963,6 +5175,12 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
4963
5175
|
} else {
|
|
4964
5176
|
startLayoutAudit();
|
|
4965
5177
|
}
|
|
5178
|
+
enhanceMermaid();
|
|
5179
|
+
if (document.readyState === "loading") {
|
|
5180
|
+
document.addEventListener("DOMContentLoaded", enhanceMermaid, { once: true });
|
|
5181
|
+
}
|
|
5182
|
+
const mermaidObserver = new MutationObserver(() => scheduleMermaidEnhance());
|
|
5183
|
+
mermaidObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
4966
5184
|
}
|
|
4967
5185
|
|
|
4968
5186
|
// src/html-transform.js
|
|
@@ -5198,6 +5416,7 @@ function normalizeFiniteNumber(value) {
|
|
|
5198
5416
|
}
|
|
5199
5417
|
function normalizeTarget(target) {
|
|
5200
5418
|
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
5419
|
+
if (target.type === "mermaid-node") return normalizeMermaidNodeTarget(target);
|
|
5201
5420
|
return JSON.parse(JSON.stringify(target));
|
|
5202
5421
|
}
|
|
5203
5422
|
|
|
@@ -5927,11 +6146,14 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5927
6146
|
key: session.key,
|
|
5928
6147
|
file: session.file,
|
|
5929
6148
|
initialChat: session.chat || [],
|
|
5930
|
-
layoutGateEnabled
|
|
6149
|
+
layoutGateEnabled,
|
|
6150
|
+
modeToggleHotkeyKey: MODE_TOGGLE_HOTKEY_KEY
|
|
5931
6151
|
});
|
|
5932
6152
|
const { head: pathHead, tail: pathTail } = displayPathParts(session.file);
|
|
5933
6153
|
const bodyClass = layoutGateEnabled ? "lavish layout-gate-active" : "lavish";
|
|
5934
6154
|
const layoutGateHidden = layoutGateEnabled ? "" : " hidden";
|
|
6155
|
+
const modeHotkeyUpper = MODE_TOGGLE_HOTKEY_KEY.toUpperCase();
|
|
6156
|
+
const modeToggleHint = `Toggle annotate/explore mode (\u2318${modeHotkeyUpper} / Ctrl+${modeHotkeyUpper})`;
|
|
5935
6157
|
return `<!doctype html>
|
|
5936
6158
|
<html>
|
|
5937
6159
|
<head>
|
|
@@ -5941,7 +6163,7 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5941
6163
|
<link rel="stylesheet" href="/chrome.css">
|
|
5942
6164
|
</head>
|
|
5943
6165
|
<body class="${bodyClass}">
|
|
5944
|
-
<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><button class="annotate-switch" id="annotation" type="button" aria-pressed="true"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml(pathHead)}</span><span class="path-tail">${escapeHtml(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
6166
|
+
<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><button class="annotate-switch" id="annotation" type="button" aria-pressed="true" title="${escapeHtml(modeToggleHint)}"><span class="switch-track" aria-hidden="true"><span class="switch-knob"></span></span><span>Annotate</span></button><div class="more-wrap" id="moreWrap"><button class="more-button" id="moreButton" type="button" title="More" aria-haspopup="menu" aria-expanded="false">${chromeIcons.more}</button><div class="menu more-menu" id="moreMenu" hidden><div class="menu-head"><div class="menu-label">Editing</div><button class="menu-file" id="copyPath" type="button" title="Copy path \xB7 ${escapeHtml(session.file)}">${chromeIcons.file}<span class="menu-file-text"><span class="path-head">${escapeHtml(pathHead)}</span><span class="path-tail">${escapeHtml(pathTail)}</span></span><span class="copy-hint" id="copyHint"><span class="icon-copy">${chromeIcons.copy}</span><span class="icon-check">${chromeIcons.check}</span><span id="copyHintText">Copy</span></span></button></div><div class="menu-rule"></div><button class="menu-item" id="reloadArtifact" type="button">${chromeIcons.refresh}<span>Reload artifact</span></button><button class="menu-item" id="copySnapshot" type="button">${chromeIcons.camera}<span>Copy DOM snapshot</span></button><button class="menu-item" id="exportArtifact" type="button">${chromeIcons.download}<span>Export standalone HTML</span></button><button class="menu-item" id="shareArtifact" type="button">${chromeIcons.globe}<span>Publish link</span></button><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
5945
6167
|
<div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" data-artifact-src="/artifact/${session.key}/index.html"></iframe><div class="layout-issue-banner" id="layoutIssueBanner" hidden>This surface may have layout issues. Your agent has been notified.</div></div><aside class="panel"><h2>Conversation</h2><div class="chat" id="chatLog"></div><div class="composer"><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><div class="annotation-pills" id="annotationPills"></div><textarea id="chatInput" placeholder="Write a message for the agent..."></textarea><div class="actions" id="sendActions"><span class="send-hint" id="sendHint" hidden>Write a message or annotate an element first.</span><div class="split"><button class="button send-main" id="send">Send to Agent</button><button class="button send-caret" id="sendCaret" type="button" title="Send options" aria-haspopup="menu" aria-expanded="false">${chromeIcons.caret}</button></div><div class="menu send-menu" id="sendMenu" hidden><button class="menu-item" id="sendFromMenu" type="button">${chromeIcons.send}<span>Send to Agent</span></button><button class="menu-item danger" id="sendAndEnd" type="button">${chromeIcons.exit}<span>Send & end session</span></button></div></div></div></aside></div>
|
|
5946
6168
|
<div class="share-overlay" id="shareDialog" role="dialog" aria-modal="true" aria-labelledby="shareTitleText" hidden><form class="share-card" id="shareForm"><div class="share-head"><div><div class="share-kicker">Publish to <a class="share-link" href="https://ht-ml.app" target="_blank" rel="noopener noreferrer">ht-ml.app</a></div><h2 id="shareTitleText">Publish artifact</h2></div><button class="share-close" id="shareClose" type="button" aria-label="Close publish dialog"><svg width="14" height="14" viewBox="0 0 10 10" fill="none" aria-hidden="true" focusable="false"><path d="M1 1L9 9M9 1L1 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></button></div><p class="share-note">ht-ml.app is a separate, third-party hosting service, not part of Lavish. Publishing sends this artifact to its servers.</p><p class="share-copy">This uploads this artifact to ht-ml.app with local assets inlined. Without a password, the page is PUBLIC and anyone with the link can open it. With a password, the page is PRIVATE and viewers must supply the password to view.</p><p class="share-note">Do not publish secrets. The Lavish annotation SDK is not included.</p><div class="share-grid"><label>Password (optional)<input id="sharePassword" name="password" type="password" autocomplete="new-password" placeholder="Leave blank for a public page"></label></div><div class="share-status" id="shareStatus" role="status"></div><div class="share-result" id="shareResult" hidden><label>Share URL<div class="share-copy-row"><input id="shareUrl" readonly><button class="share-copy-btn" id="copyShareUrl" type="button">Copy URL</button></div></label><label>Update key (secret)<div class="share-copy-row"><input id="shareUpdateKey" readonly><button class="share-copy-btn" id="copyUpdateKey" type="button">Copy key</button></div></label><p class="share-note">Keep the update key private. ht-ml.app returns it once and it is the only way to update or delete this page later.</p></div><div class="share-actions"><button class="share-cancel" id="shareCancel" type="button">Cancel</button><button class="button" id="sharePublish" type="submit">Publish</button></div></form></div>
|
|
5947
6169
|
<div class="ended-overlay layout-gate-overlay" id="layoutGateOverlay"${layoutGateHidden}><div class="ended-card"><div class="ended-title" id="layoutGateTitle">Checking layout.<br>One moment.</div><p class="ended-copy" id="layoutGateCopy">Lavish is waiting for fonts and final geometry before revealing this artifact.</p><button class="button ended-action" id="layoutGateAction" type="button">Show anyway</button></div></div>
|
|
@@ -5952,16 +6174,23 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
|
|
|
5952
6174
|
</html>`;
|
|
5953
6175
|
}
|
|
5954
6176
|
function createSdkJs(key) {
|
|
6177
|
+
const mermaidHelperEntries = Object.entries(mermaid_node_exports).filter(([, value]) => typeof value === "function");
|
|
6178
|
+
const mermaidHelperDecls = mermaidHelperEntries.map(([name, fn]) => `const ${name}=${fn.toString()};`).join("\n");
|
|
6179
|
+
const mermaidHelperKeys = mermaidHelperEntries.map(([name]) => name).join(", ");
|
|
5955
6180
|
return `(() => {
|
|
5956
6181
|
const key=${JSON.stringify(key)};
|
|
5957
6182
|
void key;
|
|
5958
6183
|
const deriveQueueKey=${deriveLavishQueueKey.toString()};
|
|
5959
6184
|
const isNativeInteractiveControl=${isNativeInteractiveControl.toString()};
|
|
6185
|
+
const MODE_TOGGLE_HOTKEY_KEY=${JSON.stringify(MODE_TOGGLE_HOTKEY_KEY)};
|
|
6186
|
+
const isModeToggleHotkeyEvent=${isModeToggleHotkeyEvent.toString()};
|
|
5960
6187
|
const fragmentsSignificantlyOverlap=${fragmentsSignificantlyOverlap.toString()};
|
|
5961
6188
|
const resolveVisibleSpillCandidates=${resolveVisibleSpillCandidates.toString()};
|
|
5962
6189
|
const classifyHorizontalOverflow=${classifyHorizontalOverflow.toString()};
|
|
5963
6190
|
const classifyVerticalOverflow=${classifyVerticalOverflow.toString()};
|
|
5964
|
-
|
|
6191
|
+
${mermaidHelperDecls}
|
|
6192
|
+
const mermaidHelpers={ ${mermaidHelperKeys} };
|
|
6193
|
+
(${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl, mermaidHelpers);
|
|
5965
6194
|
})();`;
|
|
5966
6195
|
}
|
|
5967
6196
|
function escapeHtml(value) {
|
|
@@ -6147,7 +6376,7 @@ function normalizePagePath(path6) {
|
|
|
6147
6376
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
|
|
6148
6377
|
var RESERVED = new Set(RESERVED_COMMANDS);
|
|
6149
6378
|
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`.";
|
|
6150
|
-
var VERSION = "0.1.
|
|
6379
|
+
var VERSION = "0.1.36";
|
|
6151
6380
|
async function run(argv) {
|
|
6152
6381
|
await ensureStateDir();
|
|
6153
6382
|
const normalizedArgv = normalizeArgv(argv);
|