lavish-axi 0.1.36 → 0.1.37

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
@@ -157,6 +157,7 @@ pnpm link
157
157
  - **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
158
158
  For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
159
159
  Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
160
+ Queued annotation preview pills and chat history share a scrollable Conversation panel above a sticky composer, so long feedback queues do not push the text box or send controls off screen.
160
161
  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.
161
162
  - **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
162
163
  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.
@@ -15,6 +15,7 @@ function isModeToggleHotkeyEvent(event) {
15
15
  }
16
16
 
17
17
  const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
18
+ const panelScroll = /** @type {HTMLDivElement} */ (document.getElementById("panelScroll"));
18
19
  const annotationPills = /** @type {HTMLDivElement} */ (document.getElementById("annotationPills"));
19
20
  const chatLog = /** @type {HTMLDivElement} */ (document.getElementById("chatLog"));
20
21
  const chatInput = /** @type {HTMLTextAreaElement} */ (document.getElementById("chatInput"));
@@ -146,6 +147,7 @@ function render() {
146
147
  closeButton.addEventListener("click", (event) => removeQueuedPrompt(Number(closeButton.dataset.index), event));
147
148
  }
148
149
  updateSendState();
150
+ scrollPanelToBottom();
149
151
  }
150
152
 
151
153
  function updateSendState() {
@@ -204,14 +206,15 @@ async function copyText(text) {
204
206
  return true;
205
207
  }
206
208
 
207
- function addChat(role, text) {
209
+ function addChat(role, text, shouldScroll = true) {
208
210
  if (!text) return;
209
211
 
210
212
  const el = document.createElement("div");
211
213
  el.className = "bubble " + role;
212
214
  el.innerHTML = "<small>" + (role === "agent" ? "Agent" : "You") + "</small><div>" + escapeHtml(text) + "</div>";
213
215
  chatLog.appendChild(el);
214
- chatLog.scrollTop = chatLog.scrollHeight;
216
+ if (shouldScroll) scrollElementIntoView(el);
217
+ return el;
215
218
  }
216
219
 
217
220
  function syncChat(chat) {
@@ -219,9 +222,14 @@ function syncChat(chat) {
219
222
  el.remove();
220
223
  }
221
224
 
222
- for (const item of chat) addChat(item.role, item.text);
223
- if (workingBubble) chatLog.appendChild(workingBubble);
224
- chatLog.scrollTop = chatLog.scrollHeight;
225
+ let lastChatBubble = null;
226
+ for (const item of chat) lastChatBubble = addChat(item.role, item.text, false) || lastChatBubble;
227
+ if (workingBubble) {
228
+ chatLog.appendChild(workingBubble);
229
+ scrollElementIntoView(workingBubble);
230
+ } else if (lastChatBubble) {
231
+ scrollElementIntoView(lastChatBubble);
232
+ }
225
233
  }
226
234
 
227
235
  function setAgentPresence(state) {
@@ -241,7 +249,15 @@ function setAgentPresence(state) {
241
249
  workingBubble.innerHTML = '<span class="spinner"></span><span>Working...</span>';
242
250
  chatLog.appendChild(workingBubble);
243
251
  }
244
- chatLog.scrollTop = chatLog.scrollHeight;
252
+ scrollElementIntoView(workingBubble);
253
+ }
254
+
255
+ function scrollPanelToBottom() {
256
+ panelScroll.scrollTop = panelScroll.scrollHeight;
257
+ }
258
+
259
+ function scrollElementIntoView(el) {
260
+ el.scrollIntoView({ block: "nearest", inline: "nearest" });
245
261
  }
246
262
 
247
263
  function removeQueuedPrompt(index, event) {
package/dist/chrome.css CHANGED
@@ -639,11 +639,21 @@ body.lavish {
639
639
  margin: 16px 16px 8px;
640
640
  font-weight: var(--w-semi);
641
641
  letter-spacing: 0;
642
+ flex: 0 0 auto;
643
+ }
644
+ .panel-scroll {
645
+ flex: 1 1 auto;
646
+ min-width: 0;
647
+ min-height: 0;
648
+ overflow-y: auto;
649
+ overflow-x: hidden;
650
+ display: flex;
651
+ flex-direction: column;
642
652
  }
643
653
  .chat {
644
- flex: 1;
654
+ flex: 0 0 auto;
645
655
  min-height: 0;
646
- overflow: auto;
656
+ overflow: visible;
647
657
  padding: 0 16px 12px;
648
658
  display: flex;
649
659
  flex-direction: column;
@@ -704,9 +714,13 @@ body.lavish {
704
714
  gap: 8px;
705
715
  padding: 12px 16px;
706
716
  border-top: var(--hairline-subtle);
717
+ background: var(--bg-panel);
707
718
  min-width: 0;
708
719
  flex-shrink: 0;
709
720
  box-sizing: border-box;
721
+ position: sticky;
722
+ bottom: 0;
723
+ z-index: 4;
710
724
  }
711
725
  .presence-banner {
712
726
  border: 1px solid var(--border);
@@ -726,9 +740,17 @@ body.lavish {
726
740
  flex-wrap: wrap;
727
741
  gap: 6px;
728
742
  min-width: 0;
743
+ padding: 0 16px 12px;
744
+ flex: 0 0 auto;
745
+ }
746
+ .annotation-pills:empty {
747
+ display: none;
729
748
  }
730
749
  .pill-wrap {
731
- position: relative;
750
+ display: flex;
751
+ flex-direction: column;
752
+ align-items: flex-start;
753
+ width: min(320px, 100%);
732
754
  max-width: 100%;
733
755
  }
734
756
  .pill {
@@ -769,11 +791,10 @@ body.lavish {
769
791
  }
770
792
  .pill-tooltip {
771
793
  display: none;
772
- position: absolute;
794
+ position: static;
773
795
  z-index: 5;
774
- left: 0;
775
- bottom: calc(100% + 8px);
776
- width: min(320px, 80vw);
796
+ width: 100%;
797
+ margin-top: 8px;
777
798
  border: 1px solid var(--border-strong);
778
799
  border-radius: var(--radius-lg);
779
800
  background: var(--bg-bar);
package/dist/cli.mjs CHANGED
@@ -5717,7 +5717,15 @@ async function serve({
5717
5717
  return;
5718
5718
  }
5719
5719
  await watchSession(session, watchers, events, logEvent);
5720
- res.type("html").send(createChromeHtml(session, { layoutGateEnabled: shouldEnableLayoutGate(req.query || {}) }));
5720
+ const artifactHtml = await readFile3(session.file, "utf8").catch(() => "");
5721
+ const { faviconTag, title } = extractArtifactHead(artifactHtml);
5722
+ res.type("html").send(
5723
+ createChromeHtml(session, {
5724
+ layoutGateEnabled: shouldEnableLayoutGate(req.query || {}),
5725
+ faviconTag,
5726
+ title: title ? `${title} \xB7 Lavish` : "Lavish Editor"
5727
+ })
5728
+ );
5721
5729
  } catch (error) {
5722
5730
  next(error);
5723
5731
  }
@@ -6141,7 +6149,34 @@ function normalizeFlagValue(value) {
6141
6149
  if (Array.isArray(value)) return normalizeFlagValue(value[0]);
6142
6150
  return value === void 0 || value === null ? "" : String(value).trim().toLowerCase();
6143
6151
  }
6144
- function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
6152
+ var LAVISH_DEFAULT_FAVICON = `<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>\u{1F48E}</text></svg>">`;
6153
+ function readTagAttr(tag, name) {
6154
+ const attrRe = /([a-z][\w:-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi;
6155
+ const target = name.toLowerCase();
6156
+ let match;
6157
+ while ((match = attrRe.exec(tag)) !== null) {
6158
+ if (match[1].toLowerCase() === target) {
6159
+ return (match[3] ?? match[4] ?? match[5] ?? "").trim();
6160
+ }
6161
+ }
6162
+ return "";
6163
+ }
6164
+ function extractArtifactHead(html) {
6165
+ const head = String(html || "").slice(0, 1e4);
6166
+ let faviconTag = LAVISH_DEFAULT_FAVICON;
6167
+ const linkTags = head.match(/<link\b(?:"[^"]*"|'[^']*'|[^"'>])*>/gi) || [];
6168
+ const iconTag = linkTags.find((tag) => /(^|\s)icon(\s|$)/i.test(readTagAttr(tag, "rel")));
6169
+ const iconHref = iconTag ? readTagAttr(iconTag, "href") : "";
6170
+ if (iconHref && /^(data:|https?:|\/\/)/i.test(iconHref)) {
6171
+ const safeHref = iconHref.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
6172
+ faviconTag = `<link rel="icon" href="${safeHref}">`;
6173
+ }
6174
+ let title = "";
6175
+ const titleMatch = head.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
6176
+ if (titleMatch) title = titleMatch[1].replace(/\s+/g, " ").trim();
6177
+ return { faviconTag, title };
6178
+ }
6179
+ function createChromeHtml(session, { layoutGateEnabled = true, faviconTag = LAVISH_DEFAULT_FAVICON, title = "Lavish Editor" } = {}) {
6145
6180
  const sessionJson = jsonScript({
6146
6181
  key: session.key,
6147
6182
  file: session.file,
@@ -6159,12 +6194,13 @@ function createChromeHtml(session, { layoutGateEnabled = true } = {}) {
6159
6194
  <head>
6160
6195
  <meta charset="utf-8">
6161
6196
  <meta name="viewport" content="width=device-width, initial-scale=1">
6162
- <title>Lavish Editor</title>
6197
+ <title>${escapeHtml(title)}</title>
6198
+ ${faviconTag}
6163
6199
  <link rel="stylesheet" href="/chrome.css">
6164
6200
  </head>
6165
6201
  <body class="${bodyClass}">
6166
6202
  <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>
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>
6203
+ <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="panel-scroll" id="panelScroll"><div class="chat" id="chatLog"></div><div class="annotation-pills" id="annotationPills"></div></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><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>
6168
6204
  <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>
6169
6205
  <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>
6170
6206
  <div class="ended-overlay" id="endedOverlay" hidden><div class="ended-card"><div class="ended-title">Session ended.<br>Return to your agent to continue.</div><p class="ended-copy">${escapeHtml(session.file)}</p></div></div>
@@ -6376,7 +6412,7 @@ function normalizePagePath(path6) {
6376
6412
  var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
6377
6413
  var RESERVED = new Set(RESERVED_COMMANDS);
6378
6414
  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`.";
6379
- var VERSION = "0.1.36";
6415
+ var VERSION = "0.1.37";
6380
6416
  async function run(argv) {
6381
6417
  await ensureStateDir();
6382
6418
  const normalizedArgv = normalizeArgv(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
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",
@@ -23,6 +23,7 @@
23
23
  "format": "prettier --write .",
24
24
  "format:check": "prettier --check .",
25
25
  "prepack": "npm run build",
26
+ "prepare": "npm run build",
26
27
  "test": "node --test",
27
28
  "typecheck": "tsc --noEmit"
28
29
  },