lavish-axi 0.1.19 → 0.1.21
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 +26 -4
- package/dist/chrome-client.js +182 -36
- package/dist/chrome.css +239 -77
- package/dist/cli.mjs +55 -10
- package/lavish-editor-marketing/renders/lavish-editor-marketing.gif +0 -0
- package/package.json +4 -2
- package/skills/lavish-axi/SKILL.md +54 -0
package/README.md
CHANGED
|
@@ -73,9 +73,13 @@ pnpm run build
|
|
|
73
73
|
pnpm link
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
##
|
|
76
|
+
## Teach Your Agent About Lavish (recommended)
|
|
77
77
|
|
|
78
|
-
Lavish does not
|
|
78
|
+
Lavish does not wire itself into your agent automatically, so in a fresh session your agent would not know to use it.
|
|
79
|
+
There are two ways to fix that - **you only need one.**
|
|
80
|
+
We recommend the hook.
|
|
81
|
+
|
|
82
|
+
### Option A: Session hook (recommended)
|
|
79
83
|
|
|
80
84
|
Run this once to opt in:
|
|
81
85
|
|
|
@@ -88,7 +92,22 @@ With the hook installed, your agent learns to turn complex responses into rich,
|
|
|
88
92
|
|
|
89
93
|
**Restart your agent session after running this** so the new hook takes effect.
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
### Option B: Install as a skill
|
|
96
|
+
|
|
97
|
+
Prefer the [Agent Skills](https://agentskills.io) format, or use an agent that supports it?
|
|
98
|
+
Install Lavish as a skill with [`npx skills`](https://github.com/vercel-labs/skills):
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npx skills add kunchenguid/lavish-axi --skill lavish-axi
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This drops a `lavish-axi` skill into your agent's skills directory (`.claude/skills/` for example; add `-g` for `~/.claude/skills/`).
|
|
105
|
+
The skill carries the same guidance the hook delivers, but it loads on demand when the agent recognizes a task that calls for a visual artifact, rather than every session.
|
|
106
|
+
It does not surface your live open sessions - run `lavish-axi setup hooks` if you want that ambient context too.
|
|
107
|
+
|
|
108
|
+
### No setup at all
|
|
109
|
+
|
|
110
|
+
Lavish also works fully as a plain CLI - just tell your agent to `npx lavish-axi <file.html>` as shown in the Quick Start.
|
|
92
111
|
|
|
93
112
|
## How It Works
|
|
94
113
|
|
|
@@ -119,7 +138,9 @@ Prefer not to install hooks? Lavish still works fully as a plain CLI - just tell
|
|
|
119
138
|
- **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and feedback controls. Lavish does not inject any design system, so the saved HTML file renders identically whether you open it through `lavish-axi` or directly in a browser. Choose a design system in priority order: follow a user-requested look first, match the current project's design system or conventions next, and otherwise run `lavish-axi design` for a copy-pasteable Tailwind CSS v4 + DaisyUI v5 CDN fallback.
|
|
120
139
|
- **Local assets** - Copy local images, CSS, fonts, and scripts next to the HTML artifact and reference them with relative paths from that directory; root-prefixed paths such as `/assets/logo.png` will not resolve through Lavish's artifact route.
|
|
121
140
|
- **Live reload** - Lavish watches the HTML artifact file by default and preserves the artifact iframe scroll position across reloads. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
|
|
122
|
-
- **Feedback controls** - Native form controls (radios, checkboxes, inputs, selects, buttons, labels, contenteditable) are interactive automatically, so they do not need `data-lavish-action`; wire their handlers to `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` to send feedback.
|
|
141
|
+
- **Feedback controls** - Native form controls (radios, checkboxes, inputs, selects, buttons, labels, contenteditable) are interactive automatically, so they do not need `data-lavish-action`; wire their handlers to `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` to send feedback.
|
|
142
|
+
Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them.
|
|
143
|
+
The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
|
|
123
144
|
- **Agent presence** - The browser shows when no agent is listening, keeps queued feedback for the next successful `lavish-axi poll` send even across reloads, and only blocks sending while the agent is working on delivered feedback.
|
|
124
145
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
125
146
|
- **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.
|
|
@@ -157,6 +178,7 @@ Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `input`, `
|
|
|
157
178
|
```sh
|
|
158
179
|
pnpm run check # Run all verification commands
|
|
159
180
|
pnpm run build # Bundle the publishable CLI, chrome, and design assets
|
|
181
|
+
pnpm run build:skill # Regenerate the installable lavish-axi skill
|
|
160
182
|
pnpm test # Run node:test tests
|
|
161
183
|
pnpm run lint # Run ESLint
|
|
162
184
|
pnpm run format:check # Check Prettier formatting
|
package/dist/chrome-client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const sessionDataElement = document.getElementById("lavish-session");
|
|
4
4
|
const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
|
|
5
5
|
const key = String(sessionData.key || "");
|
|
6
|
+
const filePath = String(sessionData.file || "");
|
|
6
7
|
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
7
8
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
8
9
|
|
|
@@ -11,20 +12,40 @@ const annotationPills = /** @type {HTMLDivElement} */ (document.getElementById("
|
|
|
11
12
|
const chatLog = /** @type {HTMLDivElement} */ (document.getElementById("chatLog"));
|
|
12
13
|
const chatInput = /** @type {HTMLTextAreaElement} */ (document.getElementById("chatInput"));
|
|
13
14
|
const sendButton = /** @type {HTMLButtonElement} */ (document.getElementById("send"));
|
|
14
|
-
const
|
|
15
|
+
const sendCaret = /** @type {HTMLButtonElement} */ (document.getElementById("sendCaret"));
|
|
16
|
+
const sendActions = /** @type {HTMLDivElement} */ (document.getElementById("sendActions"));
|
|
17
|
+
const sendMenu = /** @type {HTMLDivElement} */ (document.getElementById("sendMenu"));
|
|
18
|
+
const sendFromMenuButton = /** @type {HTMLButtonElement} */ (document.getElementById("sendFromMenu"));
|
|
19
|
+
const sendAndEndButton = /** @type {HTMLButtonElement} */ (document.getElementById("sendAndEnd"));
|
|
20
|
+
const annotationSwitch = /** @type {HTMLButtonElement} */ (document.getElementById("annotation"));
|
|
21
|
+
const moreWrap = /** @type {HTMLDivElement} */ (document.getElementById("moreWrap"));
|
|
22
|
+
const moreButton = /** @type {HTMLButtonElement} */ (document.getElementById("moreButton"));
|
|
23
|
+
const moreMenu = /** @type {HTMLDivElement} */ (document.getElementById("moreMenu"));
|
|
24
|
+
const reloadArtifactButton = /** @type {HTMLButtonElement} */ (document.getElementById("reloadArtifact"));
|
|
25
|
+
const copySnapshotButton = /** @type {HTMLButtonElement} */ (document.getElementById("copySnapshot"));
|
|
15
26
|
const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
|
|
16
|
-
const filePathInput = /** @type {HTMLInputElement} */ (document.getElementById("filePath"));
|
|
17
27
|
const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
|
|
28
|
+
const copyHint = /** @type {HTMLSpanElement} */ (document.getElementById("copyHint"));
|
|
29
|
+
const copyHintText = /** @type {HTMLSpanElement} */ (document.getElementById("copyHintText"));
|
|
18
30
|
const presenceBanner = /** @type {HTMLDivElement} */ (document.getElementById("presenceBanner"));
|
|
31
|
+
const endedOverlay = /** @type {HTMLDivElement} */ (document.getElementById("endedOverlay"));
|
|
32
|
+
const sendHint = /** @type {HTMLSpanElement} */ (document.getElementById("sendHint"));
|
|
19
33
|
|
|
20
34
|
const queued = loadQueuedPrompts();
|
|
21
35
|
let annotation = true;
|
|
36
|
+
let ended = false;
|
|
22
37
|
let agentPresence = "waiting";
|
|
23
38
|
let pendingSnapshot = "";
|
|
39
|
+
const snapshotRequests = [];
|
|
40
|
+
let endAfterSubmit = false;
|
|
24
41
|
let workingBubble = null;
|
|
25
42
|
let submitQueuedPromise = null;
|
|
26
43
|
let submitQueuedAgain = false;
|
|
27
44
|
let lastScroll = { x: 0, y: 0 };
|
|
45
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
46
|
+
let copyHintTimer;
|
|
47
|
+
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
48
|
+
let sendHintTimer;
|
|
28
49
|
|
|
29
50
|
function escapeHtml(value) {
|
|
30
51
|
return String(value).replace(
|
|
@@ -85,6 +106,63 @@ function render() {
|
|
|
85
106
|
const closeButton = /** @type {HTMLButtonElement} */ (button);
|
|
86
107
|
closeButton.addEventListener("click", (event) => removeQueuedPrompt(Number(closeButton.dataset.index), event));
|
|
87
108
|
}
|
|
109
|
+
updateSendState();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateSendState() {
|
|
113
|
+
sendButton.disabled = ended || agentPresence === "working";
|
|
114
|
+
sendCaret.disabled = ended || agentPresence === "working";
|
|
115
|
+
sendFromMenuButton.disabled = sendButton.disabled;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function showSendHint() {
|
|
119
|
+
sendHint.hidden = false;
|
|
120
|
+
clearTimeout(sendHintTimer);
|
|
121
|
+
sendHintTimer = setTimeout(() => {
|
|
122
|
+
sendHint.hidden = true;
|
|
123
|
+
}, 2600);
|
|
124
|
+
chatInput.focus();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hideSendHint() {
|
|
128
|
+
clearTimeout(sendHintTimer);
|
|
129
|
+
sendHint.hidden = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setMenuOpen(button, menu, open) {
|
|
133
|
+
menu.hidden = !open;
|
|
134
|
+
button.setAttribute("aria-expanded", String(open));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function closeMenus() {
|
|
138
|
+
setMenuOpen(moreButton, moreMenu, false);
|
|
139
|
+
setMenuOpen(sendCaret, sendMenu, false);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toggleMenu(button, menu) {
|
|
143
|
+
const open = menu.hidden;
|
|
144
|
+
closeMenus();
|
|
145
|
+
setMenuOpen(button, menu, open);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function copyText(text) {
|
|
149
|
+
try {
|
|
150
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
151
|
+
await navigator.clipboard.writeText(text);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Fall through to the textarea-based fallback below.
|
|
156
|
+
}
|
|
157
|
+
const helper = document.createElement("textarea");
|
|
158
|
+
helper.value = text;
|
|
159
|
+
helper.style.position = "fixed";
|
|
160
|
+
helper.style.opacity = "0";
|
|
161
|
+
document.body.appendChild(helper);
|
|
162
|
+
helper.select();
|
|
163
|
+
document.execCommand("copy");
|
|
164
|
+
helper.remove();
|
|
165
|
+
return true;
|
|
88
166
|
}
|
|
89
167
|
|
|
90
168
|
function addChat(role, text) {
|
|
@@ -109,8 +187,8 @@ function syncChat(chat) {
|
|
|
109
187
|
|
|
110
188
|
function setAgentPresence(state) {
|
|
111
189
|
agentPresence = state === "listening" || state === "working" ? state : "waiting";
|
|
112
|
-
|
|
113
|
-
if (presenceBanner) presenceBanner.hidden = agentPresence !== "waiting";
|
|
190
|
+
updateSendState();
|
|
191
|
+
if (presenceBanner) presenceBanner.hidden = ended || agentPresence !== "waiting";
|
|
114
192
|
|
|
115
193
|
if (agentPresence !== "working") {
|
|
116
194
|
if (workingBubble) workingBubble.remove();
|
|
@@ -138,8 +216,14 @@ function postToFrame(message) {
|
|
|
138
216
|
if (frame.contentWindow) frame.contentWindow.postMessage(message, "*");
|
|
139
217
|
}
|
|
140
218
|
|
|
141
|
-
function
|
|
142
|
-
|
|
219
|
+
function requestSnapshot(action) {
|
|
220
|
+
snapshotRequests.push(action);
|
|
221
|
+
postToFrame({ type: "lavish:requestSnapshot" });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sendQueued(endAfter) {
|
|
225
|
+
if (ended || agentPresence === "working") return;
|
|
226
|
+
closeMenus();
|
|
143
227
|
|
|
144
228
|
const text = chatInput.value.trim();
|
|
145
229
|
if (text) {
|
|
@@ -149,9 +233,18 @@ function sendQueued() {
|
|
|
149
233
|
chatInput.value = "";
|
|
150
234
|
render();
|
|
151
235
|
}
|
|
152
|
-
if (!queued.length)
|
|
236
|
+
if (!queued.length) {
|
|
237
|
+
if (endAfter) {
|
|
238
|
+
endSession();
|
|
239
|
+
} else {
|
|
240
|
+
showSendHint();
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
hideSendHint();
|
|
153
245
|
|
|
154
|
-
|
|
246
|
+
if (endAfter) endAfterSubmit = true;
|
|
247
|
+
requestSnapshot("submit");
|
|
155
248
|
}
|
|
156
249
|
|
|
157
250
|
async function submitQueued() {
|
|
@@ -170,7 +263,14 @@ async function submitQueued() {
|
|
|
170
263
|
submitQueuedPromise = null;
|
|
171
264
|
const shouldSubmitAgain = submitQueuedAgain;
|
|
172
265
|
submitQueuedAgain = false;
|
|
173
|
-
if (succeeded
|
|
266
|
+
if (!succeeded) {
|
|
267
|
+
endAfterSubmit = false;
|
|
268
|
+
} else if (shouldSubmitAgain && queued.length) {
|
|
269
|
+
submitQueued();
|
|
270
|
+
} else if (endAfterSubmit) {
|
|
271
|
+
endAfterSubmit = false;
|
|
272
|
+
await endSession();
|
|
273
|
+
}
|
|
174
274
|
}
|
|
175
275
|
}
|
|
176
276
|
|
|
@@ -192,23 +292,45 @@ async function submitQueuedOnce() {
|
|
|
192
292
|
}
|
|
193
293
|
|
|
194
294
|
async function endSession() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
295
|
+
if (ended) return;
|
|
296
|
+
const response = await fetch("/api/" + key + "/end", { method: "POST" });
|
|
297
|
+
if (!response.ok) throw new Error("failed to end session");
|
|
298
|
+
ended = true;
|
|
299
|
+
closeMenus();
|
|
300
|
+
annotationSwitch.disabled = true;
|
|
301
|
+
moreButton.disabled = true;
|
|
302
|
+
chatInput.disabled = true;
|
|
303
|
+
updateSendState();
|
|
304
|
+
if (presenceBanner) presenceBanner.hidden = true;
|
|
305
|
+
postToFrame({ type: "lavish:setAnnotationMode", enabled: false });
|
|
306
|
+
endedOverlay.hidden = false;
|
|
198
307
|
}
|
|
199
308
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
309
|
+
function copyFilePath() {
|
|
310
|
+
copyText(filePath);
|
|
311
|
+
copyHint.classList.add("copied");
|
|
312
|
+
copyHintText.textContent = "Copied";
|
|
313
|
+
clearTimeout(copyHintTimer);
|
|
314
|
+
copyHintTimer = setTimeout(() => {
|
|
315
|
+
copyHint.classList.remove("copied");
|
|
316
|
+
copyHintText.textContent = "Copy";
|
|
317
|
+
}, 1600);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function copyDomSnapshot() {
|
|
321
|
+
closeMenus();
|
|
322
|
+
requestSnapshot("copy");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resetFrame() {
|
|
326
|
+
// The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
|
|
327
|
+
// eslint-disable-next-line no-self-assign
|
|
328
|
+
frame.src = frame.src;
|
|
329
|
+
}
|
|
207
330
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}, 1200);
|
|
331
|
+
function reloadArtifact() {
|
|
332
|
+
closeMenus();
|
|
333
|
+
resetFrame();
|
|
212
334
|
}
|
|
213
335
|
|
|
214
336
|
async function reloadAfterServerRestart() {
|
|
@@ -242,8 +364,13 @@ window.addEventListener("message", (event) => {
|
|
|
242
364
|
render();
|
|
243
365
|
}
|
|
244
366
|
if (msg.type === "lavish:snapshot") {
|
|
245
|
-
|
|
246
|
-
|
|
367
|
+
const snapshotAction = snapshotRequests.shift() || "submit";
|
|
368
|
+
if (snapshotAction === "copy") {
|
|
369
|
+
copyText(msg.snapshot || "");
|
|
370
|
+
} else {
|
|
371
|
+
pendingSnapshot = msg.snapshot || "";
|
|
372
|
+
submitQueued();
|
|
373
|
+
}
|
|
247
374
|
}
|
|
248
375
|
if (msg.type === "lavish:scroll") {
|
|
249
376
|
lastScroll = { x: Number(msg.x) || 0, y: Number(msg.y) || 0 };
|
|
@@ -252,28 +379,47 @@ window.addEventListener("message", (event) => {
|
|
|
252
379
|
if (msg.type === "lavish:endSession") endSession();
|
|
253
380
|
});
|
|
254
381
|
|
|
255
|
-
|
|
382
|
+
annotationSwitch.onclick = () => {
|
|
256
383
|
annotation = !annotation;
|
|
257
|
-
|
|
258
|
-
annotationButton.classList.toggle("annotation-on", annotation);
|
|
384
|
+
annotationSwitch.setAttribute("aria-pressed", String(annotation));
|
|
259
385
|
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
|
|
260
386
|
};
|
|
261
387
|
|
|
262
|
-
sendButton.onclick = sendQueued;
|
|
388
|
+
sendButton.onclick = () => sendQueued(false);
|
|
389
|
+
sendFromMenuButton.onclick = () => sendQueued(false);
|
|
390
|
+
sendAndEndButton.onclick = () => sendQueued(true);
|
|
391
|
+
sendCaret.onclick = () => toggleMenu(sendCaret, sendMenu);
|
|
392
|
+
moreButton.onclick = () => toggleMenu(moreButton, moreMenu);
|
|
393
|
+
chatInput.addEventListener("keydown", (event) => {
|
|
394
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
sendQueued(false);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
chatInput.addEventListener("input", hideSendHint);
|
|
263
400
|
copyPathButton.onclick = copyFilePath;
|
|
264
|
-
|
|
401
|
+
reloadArtifactButton.onclick = reloadArtifact;
|
|
402
|
+
copySnapshotButton.onclick = copyDomSnapshot;
|
|
403
|
+
endButton.onclick = () => {
|
|
404
|
+
closeMenus();
|
|
405
|
+
endSession();
|
|
406
|
+
};
|
|
407
|
+
document.addEventListener("mousedown", (event) => {
|
|
408
|
+
const target = /** @type {Node} */ (event.target);
|
|
409
|
+
if (!moreMenu.hidden && !moreWrap.contains(target)) setMenuOpen(moreButton, moreMenu, false);
|
|
410
|
+
if (!sendMenu.hidden && !sendActions.contains(target)) setMenuOpen(sendCaret, sendMenu, false);
|
|
411
|
+
});
|
|
412
|
+
document.addEventListener("keydown", (event) => {
|
|
413
|
+
if (event.key === "Escape") closeMenus();
|
|
414
|
+
});
|
|
265
415
|
frame.addEventListener("load", () => {
|
|
266
|
-
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation });
|
|
416
|
+
postToFrame({ type: "lavish:setAnnotationMode", enabled: annotation && !ended });
|
|
267
417
|
// Replay the pre-reload scroll position so hot reloads don't jump the artifact to the top.
|
|
268
418
|
postToFrame({ type: "lavish:restoreScroll", x: lastScroll.x, y: lastScroll.y });
|
|
269
419
|
});
|
|
270
420
|
|
|
271
421
|
const events = new EventSource("/events/" + key);
|
|
272
|
-
events.addEventListener("reload", () =>
|
|
273
|
-
// The iframe is sandboxed, so reload by resetting the iframe URL from chrome.
|
|
274
|
-
// eslint-disable-next-line no-self-assign
|
|
275
|
-
frame.src = frame.src;
|
|
276
|
-
});
|
|
422
|
+
events.addEventListener("reload", () => resetFrame());
|
|
277
423
|
events.addEventListener("chrome-reload", () => reloadAfterServerRestart());
|
|
278
424
|
events.addEventListener("agent-reply", (event) => addChat("agent", JSON.parse(event.data).text));
|
|
279
425
|
events.addEventListener("chat-sync", (event) => syncChat(JSON.parse(event.data).chat || []));
|
package/dist/chrome.css
CHANGED
|
@@ -165,56 +165,217 @@ body.lavish {
|
|
|
165
165
|
position: relative;
|
|
166
166
|
top: 1px;
|
|
167
167
|
}
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
height: 22px;
|
|
171
|
-
background: var(--border);
|
|
172
|
-
flex: 0 0 auto;
|
|
168
|
+
.spacer {
|
|
169
|
+
flex: 1;
|
|
173
170
|
}
|
|
174
|
-
.
|
|
175
|
-
display: flex;
|
|
171
|
+
.annotate-switch {
|
|
172
|
+
display: inline-flex;
|
|
176
173
|
align-items: center;
|
|
177
|
-
height: 22px;
|
|
178
174
|
gap: 8px;
|
|
179
|
-
flex: 1 1 auto;
|
|
180
|
-
min-width: 0;
|
|
181
175
|
color: var(--fg-muted);
|
|
182
|
-
font-family:
|
|
183
|
-
font-size:
|
|
176
|
+
font-family: inherit;
|
|
177
|
+
font-size: var(--text-sm);
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
user-select: none;
|
|
181
|
+
background: none;
|
|
182
|
+
border: 0;
|
|
183
|
+
padding: 0;
|
|
184
|
+
flex-shrink: 0;
|
|
184
185
|
}
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
.annotate-switch:disabled {
|
|
187
|
+
cursor: not-allowed;
|
|
188
|
+
opacity: 0.55;
|
|
189
|
+
}
|
|
190
|
+
.switch-track {
|
|
191
|
+
width: 34px;
|
|
192
|
+
height: 20px;
|
|
193
|
+
border-radius: var(--radius-pill);
|
|
194
|
+
background: var(--border);
|
|
195
|
+
position: relative;
|
|
196
|
+
flex-shrink: 0;
|
|
197
|
+
transition: background 150ms var(--ease);
|
|
198
|
+
}
|
|
199
|
+
.switch-knob {
|
|
200
|
+
position: absolute;
|
|
201
|
+
top: 2px;
|
|
202
|
+
left: 2px;
|
|
203
|
+
width: 16px;
|
|
204
|
+
height: 16px;
|
|
205
|
+
border-radius: var(--radius-pill);
|
|
206
|
+
background: var(--fg-faint);
|
|
207
|
+
transition:
|
|
208
|
+
left 150ms var(--ease),
|
|
209
|
+
background 150ms var(--ease);
|
|
210
|
+
}
|
|
211
|
+
.annotate-switch[aria-pressed="true"] .switch-track {
|
|
212
|
+
background: var(--accent);
|
|
213
|
+
}
|
|
214
|
+
.annotate-switch[aria-pressed="true"] .switch-knob {
|
|
215
|
+
left: 16px;
|
|
216
|
+
background: var(--accent-ink);
|
|
217
|
+
}
|
|
218
|
+
.more-wrap {
|
|
219
|
+
position: relative;
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
}
|
|
222
|
+
.more-button {
|
|
223
|
+
width: 34px;
|
|
224
|
+
height: 34px;
|
|
225
|
+
display: grid;
|
|
226
|
+
place-items: center;
|
|
190
227
|
border: 1px solid var(--border-subtle);
|
|
191
|
-
border-radius: var(--radius-sm);
|
|
192
228
|
background: transparent;
|
|
193
|
-
color: var(--fg-
|
|
194
|
-
|
|
229
|
+
color: var(--fg-dim);
|
|
230
|
+
border-radius: 9px;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
padding: 0;
|
|
233
|
+
}
|
|
234
|
+
.more-button:hover:not(:disabled),
|
|
235
|
+
.more-button[aria-expanded="true"] {
|
|
236
|
+
border-color: var(--border-strong);
|
|
237
|
+
background: var(--steel-700);
|
|
238
|
+
color: var(--fg);
|
|
239
|
+
}
|
|
240
|
+
.more-button:disabled {
|
|
241
|
+
cursor: not-allowed;
|
|
242
|
+
opacity: 0.55;
|
|
243
|
+
}
|
|
244
|
+
.menu {
|
|
245
|
+
position: absolute;
|
|
246
|
+
background: var(--bg-bar);
|
|
247
|
+
border: 1px solid var(--border);
|
|
248
|
+
border-radius: var(--radius-lg);
|
|
249
|
+
box-shadow: var(--shadow-tooltip);
|
|
250
|
+
padding: 6px;
|
|
251
|
+
z-index: 70;
|
|
252
|
+
}
|
|
253
|
+
.menu[hidden] {
|
|
254
|
+
display: none;
|
|
255
|
+
}
|
|
256
|
+
.more-menu {
|
|
257
|
+
right: 0;
|
|
258
|
+
top: 42px;
|
|
259
|
+
width: min(300px, calc(100vw - 24px));
|
|
260
|
+
}
|
|
261
|
+
.menu-head {
|
|
262
|
+
padding: 7px 10px 9px;
|
|
263
|
+
}
|
|
264
|
+
.menu-label {
|
|
265
|
+
font-size: 9px;
|
|
266
|
+
font-weight: var(--w-bold);
|
|
267
|
+
letter-spacing: var(--track-label);
|
|
268
|
+
text-transform: uppercase;
|
|
269
|
+
color: var(--fg-faint);
|
|
270
|
+
margin-bottom: 5px;
|
|
271
|
+
}
|
|
272
|
+
.menu-file {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 7px;
|
|
276
|
+
width: calc(100% + 8px);
|
|
277
|
+
text-align: left;
|
|
278
|
+
border: 0;
|
|
279
|
+
background: transparent;
|
|
280
|
+
padding: 3px 4px;
|
|
281
|
+
margin: 0 -4px;
|
|
282
|
+
border-radius: 7px;
|
|
283
|
+
cursor: pointer;
|
|
195
284
|
font-family: var(--font-mono);
|
|
196
285
|
font-size: 12px;
|
|
197
|
-
|
|
198
|
-
|
|
286
|
+
color: var(--fg-muted);
|
|
287
|
+
}
|
|
288
|
+
.menu-file:hover {
|
|
289
|
+
background: var(--steel-700);
|
|
290
|
+
}
|
|
291
|
+
.menu-file > svg {
|
|
292
|
+
opacity: 0.7;
|
|
293
|
+
flex-shrink: 0;
|
|
294
|
+
}
|
|
295
|
+
.menu-file-text {
|
|
296
|
+
display: flex;
|
|
297
|
+
flex: 1;
|
|
298
|
+
min-width: 0;
|
|
299
|
+
}
|
|
300
|
+
.path-head {
|
|
301
|
+
overflow: hidden;
|
|
302
|
+
text-overflow: ellipsis;
|
|
303
|
+
white-space: nowrap;
|
|
304
|
+
flex: 0 1 auto;
|
|
305
|
+
min-width: 0;
|
|
306
|
+
}
|
|
307
|
+
.path-tail {
|
|
308
|
+
flex: 0 0 auto;
|
|
309
|
+
max-width: 100%;
|
|
310
|
+
overflow: hidden;
|
|
199
311
|
text-overflow: ellipsis;
|
|
312
|
+
white-space: nowrap;
|
|
200
313
|
}
|
|
201
|
-
.copy-
|
|
202
|
-
border: 1px solid var(--border);
|
|
203
|
-
border-radius: var(--radius-sm);
|
|
204
|
-
background: transparent;
|
|
205
|
-
color: var(--fg-faint);
|
|
206
|
-
font: inherit;
|
|
314
|
+
.copy-hint {
|
|
207
315
|
font-family: var(--font-sans);
|
|
208
|
-
font-size:
|
|
209
|
-
font-weight:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
316
|
+
font-size: 10px;
|
|
317
|
+
font-weight: var(--w-semi);
|
|
318
|
+
color: var(--fg-faint);
|
|
319
|
+
flex-shrink: 0;
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 4px;
|
|
214
323
|
}
|
|
215
|
-
.copy-
|
|
324
|
+
.copy-hint .icon-copy,
|
|
325
|
+
.copy-hint .icon-check {
|
|
326
|
+
display: flex;
|
|
327
|
+
}
|
|
328
|
+
.copy-hint .icon-check {
|
|
329
|
+
display: none;
|
|
330
|
+
}
|
|
331
|
+
.copy-hint.copied {
|
|
332
|
+
color: var(--accent-hover);
|
|
333
|
+
}
|
|
334
|
+
.copy-hint.copied .icon-copy {
|
|
335
|
+
display: none;
|
|
336
|
+
}
|
|
337
|
+
.copy-hint.copied .icon-check {
|
|
338
|
+
display: flex;
|
|
339
|
+
}
|
|
340
|
+
.menu-rule {
|
|
341
|
+
height: 1px;
|
|
342
|
+
background: var(--border);
|
|
343
|
+
margin: 4px 0;
|
|
344
|
+
}
|
|
345
|
+
.menu-item {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
gap: 10px;
|
|
349
|
+
width: 100%;
|
|
350
|
+
text-align: left;
|
|
351
|
+
padding: 9px 10px;
|
|
352
|
+
border-radius: var(--radius-sm);
|
|
353
|
+
font-family: inherit;
|
|
354
|
+
font-size: var(--text-sm);
|
|
355
|
+
border: 0;
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
background: transparent;
|
|
216
358
|
color: var(--fg);
|
|
217
|
-
|
|
359
|
+
}
|
|
360
|
+
.menu-item:hover:not(:disabled) {
|
|
361
|
+
background: var(--steel-700);
|
|
362
|
+
}
|
|
363
|
+
.menu-item:disabled {
|
|
364
|
+
cursor: not-allowed;
|
|
365
|
+
opacity: 0.5;
|
|
366
|
+
}
|
|
367
|
+
.menu-item > svg {
|
|
368
|
+
opacity: 0.8;
|
|
369
|
+
flex-shrink: 0;
|
|
370
|
+
}
|
|
371
|
+
.menu-item.danger {
|
|
372
|
+
color: var(--danger);
|
|
373
|
+
}
|
|
374
|
+
.menu-item.danger > svg {
|
|
375
|
+
opacity: 1;
|
|
376
|
+
}
|
|
377
|
+
.menu-item.danger:hover:not(:disabled) {
|
|
378
|
+
background: rgba(240, 100, 100, 0.1);
|
|
218
379
|
}
|
|
219
380
|
.button {
|
|
220
381
|
border: 0;
|
|
@@ -243,26 +404,25 @@ body.lavish {
|
|
|
243
404
|
opacity: 0.55;
|
|
244
405
|
cursor: not-allowed;
|
|
245
406
|
}
|
|
246
|
-
.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
border: 1px solid transparent;
|
|
250
|
-
font-weight: var(--w-semi);
|
|
407
|
+
.split {
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
align-items: stretch;
|
|
251
410
|
}
|
|
252
|
-
.
|
|
253
|
-
|
|
411
|
+
.send-main {
|
|
412
|
+
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
|
254
413
|
}
|
|
255
|
-
.
|
|
256
|
-
border:
|
|
257
|
-
|
|
258
|
-
.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
border: 1px solid var(--danger);
|
|
262
|
-
font-weight: var(--w-semi);
|
|
414
|
+
.send-caret {
|
|
415
|
+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
416
|
+
padding: 9px;
|
|
417
|
+
border-left: 1px solid rgba(23, 19, 10, 0.22);
|
|
418
|
+
display: grid;
|
|
419
|
+
place-items: center;
|
|
263
420
|
}
|
|
264
|
-
.
|
|
265
|
-
|
|
421
|
+
.send-menu {
|
|
422
|
+
right: 0;
|
|
423
|
+
bottom: 44px;
|
|
424
|
+
width: 208px;
|
|
425
|
+
border-radius: 11px;
|
|
266
426
|
}
|
|
267
427
|
.layout {
|
|
268
428
|
height: calc(100vh - var(--bar-h));
|
|
@@ -475,18 +635,34 @@ body.lavish {
|
|
|
475
635
|
}
|
|
476
636
|
.actions {
|
|
477
637
|
display: flex;
|
|
638
|
+
align-items: center;
|
|
478
639
|
gap: 8px;
|
|
479
640
|
justify-content: flex-end;
|
|
641
|
+
position: relative;
|
|
480
642
|
}
|
|
481
|
-
.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
643
|
+
.send-hint {
|
|
644
|
+
margin-right: auto;
|
|
645
|
+
color: var(--fg-faint);
|
|
646
|
+
font-size: 11px;
|
|
647
|
+
line-height: 1.35;
|
|
648
|
+
}
|
|
649
|
+
.send-hint[hidden] {
|
|
650
|
+
display: none;
|
|
651
|
+
}
|
|
652
|
+
.ended-overlay {
|
|
653
|
+
position: fixed;
|
|
654
|
+
inset: var(--bar-h) 0 0 0;
|
|
655
|
+
background: rgba(15, 17, 21, 0.86);
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
justify-content: center;
|
|
659
|
+
z-index: 60;
|
|
660
|
+
}
|
|
661
|
+
.ended-overlay[hidden] {
|
|
662
|
+
display: none;
|
|
487
663
|
}
|
|
488
664
|
.ended-card {
|
|
489
|
-
width: min(360px,
|
|
665
|
+
width: min(360px, calc(100vw - 48px));
|
|
490
666
|
border: 1px solid var(--border);
|
|
491
667
|
border-radius: var(--radius-xl);
|
|
492
668
|
background: var(--bg-panel);
|
|
@@ -496,8 +672,8 @@ body.lavish {
|
|
|
496
672
|
.ended-title {
|
|
497
673
|
font-family: var(--font-serif);
|
|
498
674
|
font-style: italic;
|
|
499
|
-
font-size:
|
|
500
|
-
line-height: 1.
|
|
675
|
+
font-size: 22px;
|
|
676
|
+
line-height: 1.3;
|
|
501
677
|
color: var(--fg);
|
|
502
678
|
margin-bottom: 8px;
|
|
503
679
|
}
|
|
@@ -507,6 +683,7 @@ body.lavish {
|
|
|
507
683
|
font-family: var(--font-mono);
|
|
508
684
|
font-size: 12px;
|
|
509
685
|
line-height: 1.45;
|
|
686
|
+
overflow-wrap: anywhere;
|
|
510
687
|
}
|
|
511
688
|
iframe {
|
|
512
689
|
width: 100%;
|
|
@@ -518,21 +695,6 @@ iframe {
|
|
|
518
695
|
body {
|
|
519
696
|
overflow: auto;
|
|
520
697
|
}
|
|
521
|
-
.bar {
|
|
522
|
-
min-height: var(--bar-h);
|
|
523
|
-
height: auto;
|
|
524
|
-
align-items: flex-start;
|
|
525
|
-
flex-wrap: wrap;
|
|
526
|
-
padding: 10px 12px;
|
|
527
|
-
}
|
|
528
|
-
.divider {
|
|
529
|
-
display: none;
|
|
530
|
-
}
|
|
531
|
-
.file-wrap {
|
|
532
|
-
order: 3;
|
|
533
|
-
flex-basis: 100%;
|
|
534
|
-
font-size: 11px;
|
|
535
|
-
}
|
|
536
698
|
.layout {
|
|
537
699
|
height: calc(100vh - var(--bar-h));
|
|
538
700
|
grid-template-columns: 1fr;
|
package/dist/cli.mjs
CHANGED
|
@@ -706,6 +706,7 @@ function playbookIds() {
|
|
|
706
706
|
// src/server.js
|
|
707
707
|
import { EventEmitter } from "node:events";
|
|
708
708
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
709
|
+
import { homedir } from "node:os";
|
|
709
710
|
import path3 from "node:path";
|
|
710
711
|
import chokidar from "chokidar";
|
|
711
712
|
import express from "express";
|
|
@@ -942,6 +943,12 @@ function createArtifactSdk() {
|
|
|
942
943
|
if (prompt) queuePrompt(prompt, c);
|
|
943
944
|
closeCard();
|
|
944
945
|
};
|
|
946
|
+
textarea.addEventListener("keydown", (event) => {
|
|
947
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
948
|
+
event.preventDefault();
|
|
949
|
+
sendButton.click();
|
|
950
|
+
}
|
|
951
|
+
});
|
|
945
952
|
setTimeout(() => textarea.focus(), 0);
|
|
946
953
|
}
|
|
947
954
|
window.lavish = {
|
|
@@ -1101,12 +1108,9 @@ var SessionStore = class {
|
|
|
1101
1108
|
if (!session) {
|
|
1102
1109
|
return { status: "missing" };
|
|
1103
1110
|
}
|
|
1104
|
-
if (session.status === "ended") {
|
|
1105
|
-
return { status: "ended" };
|
|
1106
|
-
}
|
|
1107
1111
|
const prompts = session.prompts || [];
|
|
1108
1112
|
if (prompts.length === 0) {
|
|
1109
|
-
return { status: "waiting" };
|
|
1113
|
+
return session.status === "ended" ? { status: "ended" } : { status: "waiting" };
|
|
1110
1114
|
}
|
|
1111
1115
|
const result = {
|
|
1112
1116
|
status: "feedback",
|
|
@@ -1116,7 +1120,9 @@ var SessionStore = class {
|
|
|
1116
1120
|
session.prompts = [];
|
|
1117
1121
|
session.pending_prompts = 0;
|
|
1118
1122
|
session.dom_snapshot = "";
|
|
1119
|
-
session.status
|
|
1123
|
+
if (session.status !== "ended") {
|
|
1124
|
+
session.status = "open";
|
|
1125
|
+
}
|
|
1120
1126
|
session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1121
1127
|
await this.writeState(state);
|
|
1122
1128
|
return result;
|
|
@@ -1691,9 +1697,47 @@ function computePresence(key, activePolls, deliveredFeedback) {
|
|
|
1691
1697
|
if (deliveredFeedback.has(key)) return "working";
|
|
1692
1698
|
return "waiting";
|
|
1693
1699
|
}
|
|
1700
|
+
function chromeIcon(paths, size = 16, strokeWidth = 1.7) {
|
|
1701
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${strokeWidth}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
1702
|
+
}
|
|
1703
|
+
var chromeIcons = {
|
|
1704
|
+
more: chromeIcon(
|
|
1705
|
+
'<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/>'
|
|
1706
|
+
),
|
|
1707
|
+
file: chromeIcon(
|
|
1708
|
+
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>',
|
|
1709
|
+
13
|
|
1710
|
+
),
|
|
1711
|
+
copy: chromeIcon(
|
|
1712
|
+
'<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
|
|
1713
|
+
12
|
|
1714
|
+
),
|
|
1715
|
+
check: chromeIcon('<polyline points="20 6 9 17 4 12"/>', 12),
|
|
1716
|
+
refresh: chromeIcon(
|
|
1717
|
+
'<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/>',
|
|
1718
|
+
15
|
|
1719
|
+
),
|
|
1720
|
+
camera: chromeIcon(
|
|
1721
|
+
'<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3z"/><circle cx="12" cy="13" r="3"/>',
|
|
1722
|
+
15
|
|
1723
|
+
),
|
|
1724
|
+
exit: chromeIcon(
|
|
1725
|
+
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
|
|
1726
|
+
15
|
|
1727
|
+
),
|
|
1728
|
+
send: chromeIcon('<path d="M22 2 11 13"/><path d="M22 2 15 22l-4-9-9-4Z"/>', 14),
|
|
1729
|
+
caret: chromeIcon('<path d="m6 9 6 6 6-6"/>', 13, 2)
|
|
1730
|
+
};
|
|
1731
|
+
function displayPathParts(file, home = homedir()) {
|
|
1732
|
+
const normalizedFile = file.replaceAll("\\", "/");
|
|
1733
|
+
const normalizedHome = home.replaceAll("\\", "/");
|
|
1734
|
+
const display = normalizedHome && normalizedFile.startsWith(`${normalizedHome}/`) ? `~/${normalizedFile.slice(normalizedHome.length + 1)}` : normalizedFile;
|
|
1735
|
+
const tailStart = display.lastIndexOf("/") + 1;
|
|
1736
|
+
return { head: display.slice(0, tailStart), tail: display.slice(tailStart) };
|
|
1737
|
+
}
|
|
1694
1738
|
function createChromeHtml(session) {
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1739
|
+
const sessionJson = jsonScript({ key: session.key, file: session.file, initialChat: session.chat || [] });
|
|
1740
|
+
const { head: pathHead, tail: pathTail } = displayPathParts(session.file);
|
|
1697
1741
|
return `<!doctype html>
|
|
1698
1742
|
<html>
|
|
1699
1743
|
<head>
|
|
@@ -1703,8 +1747,9 @@ function createChromeHtml(session) {
|
|
|
1703
1747
|
<link rel="stylesheet" href="/chrome.css">
|
|
1704
1748
|
</head>
|
|
1705
1749
|
<body class="lavish">
|
|
1706
|
-
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="
|
|
1707
|
-
<div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" src="/artifact/${session.key}/index.html"></iframe></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"><button class="button" id="send">Send to Agent</button></div></div></aside></div>
|
|
1750
|
+
<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><div class="menu-rule"></div><button class="menu-item danger" id="end" type="button">${chromeIcons.exit}<span>End session</span></button></div></div></div>
|
|
1751
|
+
<div class="layout"><div class="frame"><iframe id="artifact" sandbox="allow-scripts allow-forms allow-popups allow-downloads" src="/artifact/${session.key}/index.html"></iframe></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>
|
|
1752
|
+
<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>
|
|
1708
1753
|
<script id="lavish-session" type="application/json">${sessionJson}</script>
|
|
1709
1754
|
<script src="/chrome-client.js"></script>
|
|
1710
1755
|
</body>
|
|
@@ -1899,7 +1944,7 @@ function normalizePagePath(path5) {
|
|
|
1899
1944
|
// src/cli.js
|
|
1900
1945
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup"]);
|
|
1901
1946
|
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`.";
|
|
1902
|
-
var VERSION = "0.1.
|
|
1947
|
+
var VERSION = "0.1.21";
|
|
1903
1948
|
async function run(argv) {
|
|
1904
1949
|
await ensureStateDir();
|
|
1905
1950
|
const normalizedArgv = normalizeArgv(argv);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lavish-axi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
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",
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"skills/lavish-axi",
|
|
12
13
|
"lavish-editor-marketing/renders/lavish-editor-marketing.gif",
|
|
13
14
|
"LICENSE",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "node scripts/build.js",
|
|
18
|
-
"
|
|
19
|
+
"build:skill": "node scripts/build-skill.js",
|
|
20
|
+
"check": "npm run build && npm run lint && npm run format:check && npm run typecheck && npm test && node scripts/build-skill.js --check",
|
|
19
21
|
"start": "node dist/cli.mjs",
|
|
20
22
|
"lint": "eslint bin src test scripts",
|
|
21
23
|
"format": "prettier --write .",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lavish-axi
|
|
3
|
+
description: Turn complex or visual agent responses into rich, reviewable HTML artifacts the user can annotate and send feedback on, using the lavish-axi CLI. Use when about to give a plan, comparison, diagram, table, code diff, report, or anything easier to grasp visually than as prose.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lavish Editor
|
|
7
|
+
|
|
8
|
+
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 `npx -y lavish-axi <html-file>` so the user can visually review it, annotate elements or selected text, queue prompts, and send feedback back through `npx -y lavish-axi poll`.
|
|
9
|
+
|
|
10
|
+
You do not need lavish-axi installed globally - invoke it with `npx -y lavish-axi <html-file>`.
|
|
11
|
+
If lavish-axi output shows a follow-up command starting with `lavish-axi`, run it as `npx -y lavish-axi ...` instead.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
1. Create the HTML artifact (default location `.lavish/<name>.html` in the working directory).
|
|
20
|
+
2. Run `npx -y lavish-axi <html-file>` to open or resume a review session in the browser.
|
|
21
|
+
3. Run `npx -y lavish-axi poll <html-file>` to long-poll for the user's annotations and queued prompts.
|
|
22
|
+
4. Apply the feedback, then poll again with `--agent-reply "<message>"` to reply in the browser and keep the loop going.
|
|
23
|
+
5. Run `npx -y lavish-axi end <html-file>` when the review is finished.
|
|
24
|
+
|
|
25
|
+
## Visual guidance
|
|
26
|
+
|
|
27
|
+
- Use visual hierarchy to make the most important decisions, risks, tradeoffs, and next actions obvious at a glance
|
|
28
|
+
- Use visual structure such as sections, cards, tables, diagrams, annotated snippets, and side-by-side comparisons instead of long prose
|
|
29
|
+
- Choose typography, spacing, color, and layout deliberately so the artifact has a clear point of view
|
|
30
|
+
- Prevent horizontal overflow: design narrow layouts intentionally, use minmax(0, 1fr) and min-width: 0 for grid/flex children, and deliberately wrap or truncate long labels/status text
|
|
31
|
+
|
|
32
|
+
## Playbooks
|
|
33
|
+
|
|
34
|
+
Run `npx -y lavish-axi playbook <id>` for focused, detailed guidance on any of these:
|
|
35
|
+
|
|
36
|
+
- `diagram` - Map relationships, flows, state, and architecture
|
|
37
|
+
- `table` - Turn dense records into scan-friendly review surfaces
|
|
38
|
+
- `comparison` - Show options, tradeoffs, and current vs target behavior
|
|
39
|
+
- `plan` - Explain a product or technical plan before implementation
|
|
40
|
+
- `diff` - Present code or PR changes with evidence and findings
|
|
41
|
+
- `input` - Must be used when the agent needs to collect user input on decisions, choices, preferences, triage, scope, or other structured feedback from within the artifact
|
|
42
|
+
- `slides` - Create a deliberate presentation when slides are requested
|
|
43
|
+
|
|
44
|
+
## Commands & rules
|
|
45
|
+
|
|
46
|
+
- Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session
|
|
47
|
+
- Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`
|
|
48
|
+
- Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work
|
|
49
|
+
- Run `npx -y lavish-axi poll <html-file>` to wait for user feedback
|
|
50
|
+
- Run `npx -y lavish-axi end <html-file>` to end a session
|
|
51
|
+
- Run `npx -y lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)
|
|
52
|
+
- Run `npx -y lavish-axi playbook <playbook_id>` for focused artifact guidance
|
|
53
|
+
- Lavish does not auto-inject any design system - artifacts stay portable so they render identically when opened directly without lavish-axi running. Choose a design system in this priority order: (1) if the user asked for a specific look or named design system, follow that; (2) otherwise, if the current project already has a design system or style conventions, match those so the artifact fits in; (3) otherwise, prefer the Lavish-recommended Tailwind CSS browser runtime v4 + DaisyUI v5, available via CDN - run `npx -y lavish-axi design` for a copy-pasteable CDN snippet plus component reference. Prefer that CDN snippet over hand-writing styles unless explicitly instructed otherwise by the user.
|
|
54
|
+
- Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop
|