lavish-axi 0.1.3 → 0.1.6

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
@@ -1,10 +1,10 @@
1
1
  <h1 align="center">lavish-axi</h1>
2
2
  <p align="center">
3
- <a href="https://github.com/kunchenguid/lavish/actions/workflows/ci.yml"
4
- ><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/kunchenguid/lavish/ci.yml?style=flat-square&label=ci"
3
+ <a href="https://github.com/kunchenguid/lavish-axi/actions/workflows/ci.yml"
4
+ ><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/kunchenguid/lavish-axi/ci.yml?style=flat-square&label=ci"
5
5
  /></a>
6
- <a href="https://github.com/kunchenguid/lavish/actions/workflows/release-please.yml"
7
- ><img alt="Release" src="https://img.shields.io/github/actions/workflow/status/kunchenguid/lavish/release-please.yml?style=flat-square&label=release"
6
+ <a href="https://github.com/kunchenguid/lavish-axi/actions/workflows/release-please.yml"
7
+ ><img alt="Release" src="https://img.shields.io/github/actions/workflow/status/kunchenguid/lavish-axi/release-please.yml?style=flat-square&label=release"
8
8
  /></a>
9
9
  <a href="https://www.npmjs.com/package/lavish-axi"
10
10
  ><img alt="npm" src="https://img.shields.io/npm/v/lavish-axi?style=flat-square"
@@ -31,15 +31,21 @@ 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 an agent-generated HTML file in a local browser, lets you pinpoint elements and send feedback.
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.
35
35
 
36
- - **Browser-native review** - Serve local HTML artifacts with relative JS/CSS/assets intact.
37
- - **Precise feedback** - Click elements, queue annotation & prompts , and send them back to the agent.
38
- - **Agent-ergonomic interface** - Follows [AXI principles](https://axi.md), use TOON output, long polling, and contextual disclosure that keep autonomous agents on track.
36
+ - **Local only** - Work with your local HTML artifacts with a local CLI. Zero cloud dependency.
37
+ - **Human-AI collaboration** - Annotate elements, selected text ranges, and send messages to the agent without leaving Lavish Editor.
38
+ - **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as technial plans, design explorations and more out of the box.
39
+
40
+ Lavish Editor is an [AXI](https://axi.md), which means -
41
+
42
+ - It's just a CLI any capable agent can run without setup.
43
+ - No skills required. Agents learn to use AXIs by using them.
44
+ - It's optimized for agent ergonomics. TOON output, long polling, and contextual disclosure making it highly token efficient.
39
45
 
40
46
  ## Quick Start
41
47
 
42
- Tell your agent:
48
+ Just tell your agent:
43
49
 
44
50
  ```sh
45
51
  Use `npx lavish-axi` to write a technical plan for what we discussed.
@@ -56,8 +62,8 @@ npm install -g lavish-axi
56
62
  **From source**
57
63
 
58
64
  ```sh
59
- git clone https://github.com/kunchenguid/lavish.git
60
- cd lavish
65
+ git clone https://github.com/kunchenguid/lavish-axi.git
66
+ cd lavish-axi
61
67
  npm ci
62
68
  npm run build
63
69
  npm link
@@ -77,7 +83,8 @@ npm link
77
83
  └───────┬────────────────┘
78
84
  ▼
79
85
  ┌────────────────────────┐
80
- │ Human annotates or │
86
+ │ Human annotates text │
87
+ │ or elements, or │
81
88
  │ sends chat feedback │
82
89
  └───────┬────────────────┘
83
90
  ▼
@@ -88,7 +95,8 @@ npm link
88
95
  ```
89
96
 
90
97
  - **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
91
- - **Sandboxed artifact** - The artifact runs in an iframe while Lavish injects a small SDK for annotations and snapshots.
98
+ - **Sandboxed artifact** - The artifact runs in an iframe while Lavish injects a small SDK for annotations and snapshots, plus Tailwind CSS v4 and DaisyUI v5 design assets unless the HTML includes `<meta name="lavish-design" content="off">`.
99
+ - **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
92
100
  - **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
93
101
 
94
102
  ## CLI Reference
@@ -99,6 +107,10 @@ npm link
99
107
  | `lavish-axi <html-file>` | Open or resume a Lavish Editor session. |
100
108
  | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session. |
101
109
  | `lavish-axi end <html-file>` | End a session. |
110
+ | `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook. |
111
+ | `lavish-axi design` | Show the injected Tailwind CSS and DaisyUI design reference. |
112
+
113
+ Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `interactive`, `slides`.
102
114
 
103
115
  ### Flags
104
116
 
@@ -112,7 +124,7 @@ npm link
112
124
 
113
125
  ```sh
114
126
  npm run check # Run all verification commands
115
- npm run build # Bundle the publishable CLI
127
+ npm run build # Bundle the publishable CLI, chrome, and design assets
116
128
  npm test # Run node:test tests
117
129
  npm run lint # Run ESLint
118
130
  npm run format:check # Check Prettier formatting
@@ -0,0 +1,221 @@
1
+ /* global EventSource, document, location, window */
2
+
3
+ const sessionDataElement = document.getElementById("lavish-session");
4
+ const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
5
+ const key = String(sessionData.key || "");
6
+ const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
7
+
8
+ const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
9
+ const annotationPills = /** @type {HTMLDivElement} */ (document.getElementById("annotationPills"));
10
+ const chatLog = /** @type {HTMLDivElement} */ (document.getElementById("chatLog"));
11
+ const chatInput = /** @type {HTMLTextAreaElement} */ (document.getElementById("chatInput"));
12
+ const sendButton = /** @type {HTMLButtonElement} */ (document.getElementById("send"));
13
+ const annotationButton = /** @type {HTMLButtonElement} */ (document.getElementById("annotation"));
14
+ const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
15
+ const filePathInput = /** @type {HTMLInputElement} */ (document.getElementById("filePath"));
16
+ const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
17
+
18
+ const queued = [];
19
+ let annotation = true;
20
+ let agentPolling = false;
21
+ let pendingSnapshot = "";
22
+ let workingBubble = null;
23
+
24
+ function escapeHtml(value) {
25
+ return String(value).replace(
26
+ /[&<>"']/g,
27
+ (char) =>
28
+ ({
29
+ "&": "&amp;",
30
+ "<": "&lt;",
31
+ ">": "&gt;",
32
+ '"': "&quot;",
33
+ "'": "&#39;",
34
+ })[char],
35
+ );
36
+ }
37
+
38
+ function render() {
39
+ annotationPills.innerHTML = queued
40
+ .map(
41
+ (prompt, index) =>
42
+ '<div class="pill-wrap"><div class="pill"><span class="pill-preview">' +
43
+ escapeHtml(prompt.prompt) +
44
+ '</span><button class="pill-close" type="button" aria-label="Remove queued prompt" data-index="' +
45
+ index +
46
+ '">×</button></div><div class="pill-tooltip">' +
47
+ (prompt.selector
48
+ ? '<div class="tooltip-label">Target</div><div class="pill-tooltip-target">' +
49
+ escapeHtml(prompt.selector) +
50
+ "</div>"
51
+ : "") +
52
+ '<div class="tooltip-label">Prompt</div><div class="pill-tooltip-prompt">' +
53
+ escapeHtml(prompt.prompt) +
54
+ "</div></div></div>",
55
+ )
56
+ .join("");
57
+
58
+ for (const button of annotationPills.querySelectorAll(".pill-close")) {
59
+ const closeButton = /** @type {HTMLButtonElement} */ (button);
60
+ closeButton.addEventListener("click", (event) => removeQueuedPrompt(Number(closeButton.dataset.index), event));
61
+ }
62
+ }
63
+
64
+ function addChat(role, text) {
65
+ if (!text) return;
66
+
67
+ const el = document.createElement("div");
68
+ el.className = "bubble " + role;
69
+ el.innerHTML = "<small>" + (role === "agent" ? "Agent" : "You") + "</small><div>" + escapeHtml(text) + "</div>";
70
+ chatLog.appendChild(el);
71
+ chatLog.scrollTop = chatLog.scrollHeight;
72
+ }
73
+
74
+ function syncChat(chat) {
75
+ for (const el of [...chatLog.querySelectorAll(".bubble.user,.bubble.agent:not(.agent-working)")]) {
76
+ el.remove();
77
+ }
78
+
79
+ for (const item of chat) addChat(item.role, item.text);
80
+ if (workingBubble) chatLog.appendChild(workingBubble);
81
+ chatLog.scrollTop = chatLog.scrollHeight;
82
+ }
83
+
84
+ function setAgentPolling(active) {
85
+ agentPolling = !!active;
86
+ sendButton.disabled = !agentPolling;
87
+
88
+ if (agentPolling) {
89
+ if (workingBubble) workingBubble.remove();
90
+ workingBubble = null;
91
+ return;
92
+ }
93
+
94
+ if (!workingBubble) {
95
+ workingBubble = document.createElement("div");
96
+ workingBubble.className = "bubble agent agent-working";
97
+ workingBubble.innerHTML = '<span class="spinner"></span><span>Working...</span>';
98
+ chatLog.appendChild(workingBubble);
99
+ }
100
+ chatLog.scrollTop = chatLog.scrollHeight;
101
+ }
102
+
103
+ function removeQueuedPrompt(index, event) {
104
+ if (event) event.stopPropagation();
105
+ queued.splice(index, 1);
106
+ render();
107
+ }
108
+
109
+ function postToFrame(message) {
110
+ if (frame.contentWindow) frame.contentWindow.postMessage(message, "*");
111
+ }
112
+
113
+ function sendQueued() {
114
+ if (!agentPolling) return;
115
+
116
+ const text = chatInput.value.trim();
117
+ if (text) {
118
+ queued.push({ uid: "", prompt: text, selector: "", tag: "message", text: "Freeform message" });
119
+ addChat("user", text);
120
+ chatInput.value = "";
121
+ render();
122
+ }
123
+ if (!queued.length) return;
124
+
125
+ postToFrame({ type: "lavish:requestSnapshot" });
126
+ }
127
+
128
+ async function submitQueued() {
129
+ const prompts = queued.splice(0, queued.length);
130
+ render();
131
+ setAgentPolling(false);
132
+ await fetch("/api/" + key + "/prompts", {
133
+ method: "POST",
134
+ headers: { "content-type": "application/json" },
135
+ body: JSON.stringify({ prompts, domSnapshot: pendingSnapshot }),
136
+ });
137
+ }
138
+
139
+ async function endSession() {
140
+ await fetch("/api/" + key + "/end", { method: "POST" });
141
+ document.body.innerHTML =
142
+ '<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div></div><main class="ended-view"><section class="ended-card"><div class="ended-title">Session ended.</div><p class="ended-copy">Return to your agent to continue.</p></section></main>';
143
+ }
144
+
145
+ async function copyFilePath() {
146
+ try {
147
+ await navigator.clipboard.writeText(filePathInput.value);
148
+ } catch {
149
+ filePathInput.select();
150
+ document.execCommand("copy");
151
+ }
152
+
153
+ copyPathButton.textContent = "Copied";
154
+ setTimeout(() => {
155
+ copyPathButton.textContent = "Copy Path";
156
+ }, 1200);
157
+ }
158
+
159
+ async function reloadAfterServerRestart() {
160
+ let sawOutage = false;
161
+ const deadline = Date.now() + 5000;
162
+
163
+ while (Date.now() < deadline) {
164
+ try {
165
+ const res = await fetch("/health", { cache: "no-store" });
166
+ if (sawOutage && res.ok) {
167
+ location.reload();
168
+ return;
169
+ }
170
+ } catch {
171
+ sawOutage = true;
172
+ }
173
+
174
+ await new Promise((resolve) => setTimeout(resolve, 100));
175
+ }
176
+
177
+ location.reload();
178
+ }
179
+
180
+ window.addEventListener("message", (event) => {
181
+ if (event.source !== frame.contentWindow) return;
182
+
183
+ const msg = event.data || {};
184
+ if (msg.type === "lavish:queuePrompt") {
185
+ queued.push(msg.prompt);
186
+ render();
187
+ }
188
+ if (msg.type === "lavish:snapshot") {
189
+ pendingSnapshot = msg.snapshot || "";
190
+ submitQueued();
191
+ }
192
+ if (msg.type === "lavish:sendQueuedPrompts") sendQueued();
193
+ if (msg.type === "lavish:endSession") endSession();
194
+ });
195
+
196
+ annotationButton.onclick = () => {
197
+ annotation = !annotation;
198
+ annotationButton.textContent = "Annotation: " + (annotation ? "On" : "Off");
199
+ annotationButton.classList.toggle("annotation-on", annotation);
200
+ postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
201
+ };
202
+
203
+ sendButton.onclick = sendQueued;
204
+ copyPathButton.onclick = copyFilePath;
205
+ endButton.onclick = endSession;
206
+ frame.addEventListener("load", () => postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation }));
207
+
208
+ const events = new EventSource("/events/" + key);
209
+ events.addEventListener("reload", () => {
210
+ // The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
211
+ // eslint-disable-next-line no-self-assign
212
+ frame.src = frame.src;
213
+ });
214
+ events.addEventListener("chrome-reload", () => reloadAfterServerRestart());
215
+ events.addEventListener("agent-reply", (event) => addChat("agent", JSON.parse(event.data).text));
216
+ events.addEventListener("chat-sync", (event) => syncChat(JSON.parse(event.data).chat || []));
217
+ events.addEventListener("agent-working", (event) => setAgentPolling(!JSON.parse(event.data).working));
218
+
219
+ render();
220
+ initialChat.forEach((item) => addChat(item.role, item.text));
221
+ setAgentPolling(false);