lavish-axi 0.1.3 → 0.1.5
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 +24 -13
- package/dist/chrome-client.js +221 -0
- package/dist/chrome.css +539 -0
- package/dist/cli.mjs +708 -121
- package/package.json +1 -1
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
|
|
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
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
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
|
-
|
|
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
|
|
86
|
+
│ Human annotates text │
|
|
87
|
+
│ or elements, or │
|
|
81
88
|
│ sends chat feedback │
|
|
82
89
|
└───────┬────────────────┘
|
|
83
90
|
▼
|
|
@@ -89,6 +96,7 @@ npm link
|
|
|
89
96
|
|
|
90
97
|
- **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
|
|
91
98
|
- **Sandboxed artifact** - The artifact runs in an iframe while Lavish injects a small SDK for annotations and snapshots.
|
|
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,9 @@ 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
|
+
|
|
112
|
+
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `interactive`, `slides`.
|
|
102
113
|
|
|
103
114
|
### Flags
|
|
104
115
|
|
|
@@ -112,7 +123,7 @@ npm link
|
|
|
112
123
|
|
|
113
124
|
```sh
|
|
114
125
|
npm run check # Run all verification commands
|
|
115
|
-
npm run build # Bundle the publishable CLI
|
|
126
|
+
npm run build # Bundle the publishable CLI and chrome assets
|
|
116
127
|
npm test # Run node:test tests
|
|
117
128
|
npm run lint # Run ESLint
|
|
118
129
|
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
|
+
"&": "&",
|
|
30
|
+
"<": "<",
|
|
31
|
+
">": ">",
|
|
32
|
+
'"': """,
|
|
33
|
+
"'": "'",
|
|
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);
|