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 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 or selected text and send feedback to the agent to address.
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.
@@ -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
- annotationSwitch.onclick = () => {
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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
- return {
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
- selected = target;
5038
+ anchor = annotationTargetEl(target);
5039
+ selected = anchor;
4839
5040
  highlightElement(selected);
4840
5041
  }
4841
- const rect = options.range ? options.range.getBoundingClientRect() : target.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 heading = c.tag === "text" ? "Annotate text" : "Annotate &lt;" + c.tag + "&gt;";
4845
- const placeholder = c.tag === "text" ? "Tell the agent what to change about this text..." : "Tell the agent what to change about this element...";
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 &lt;" + c.tag + "&gt;";
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 &middot; ' + (/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
- if (event.target === selected) return;
5127
+ const target = annotationTargetEl(event.target);
5128
+ if (target === selected) return;
4917
5129
  if (hovered && hovered !== selected) clearHighlight(hovered);
4918
- hovered = event.target;
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 &amp; 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
- (${createArtifactSdk.toString()})(deriveQueueKey, isNativeInteractiveControl);
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.35";
6379
+ var VERSION = "0.1.36";
6151
6380
  async function run(argv) {
6152
6381
  await ensureStateDir();
6153
6382
  const normalizedArgv = normalizeArgv(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "packageManager": "pnpm@11.1.1",
5
5
  "description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
6
6
  "type": "module",