lavish-axi 0.1.8 → 0.1.9
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 +1 -0
- package/dist/chrome-client.js +11 -9
- package/dist/chrome.css +13 -0
- package/dist/cli.mjs +67 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,7 @@ pnpm link
|
|
|
97
97
|
- **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
|
|
98
98
|
- **Sandboxed artifact** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and feedback controls, plus Tailwind CSS v4 and DaisyUI v5 design assets unless the HTML includes `<meta name="lavish-design" content="off">`.
|
|
99
99
|
- **Feedback controls** - Mark buttons, choices, and other interactive elements with `data-lavish-action` so Lavish does not annotate them, then call `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` from the control handler.
|
|
100
|
+
- **Agent presence** - The browser shows when no agent is listening, still queues feedback for the next `lavish-axi poll`, and only blocks sending while the agent is working on delivered feedback.
|
|
100
101
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
101
102
|
- **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
|
|
102
103
|
|
package/dist/chrome-client.js
CHANGED
|
@@ -14,10 +14,11 @@ const annotationButton = /** @type {HTMLButtonElement} */ (document.getElementBy
|
|
|
14
14
|
const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
|
|
15
15
|
const filePathInput = /** @type {HTMLInputElement} */ (document.getElementById("filePath"));
|
|
16
16
|
const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
|
|
17
|
+
const presenceBanner = /** @type {HTMLDivElement} */ (document.getElementById("presenceBanner"));
|
|
17
18
|
|
|
18
19
|
const queued = [];
|
|
19
20
|
let annotation = true;
|
|
20
|
-
let
|
|
21
|
+
let agentPresence = "waiting";
|
|
21
22
|
let pendingSnapshot = "";
|
|
22
23
|
let workingBubble = null;
|
|
23
24
|
|
|
@@ -81,11 +82,12 @@ function syncChat(chat) {
|
|
|
81
82
|
chatLog.scrollTop = chatLog.scrollHeight;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
sendButton.disabled =
|
|
85
|
+
function setAgentPresence(state) {
|
|
86
|
+
agentPresence = state === "listening" || state === "working" ? state : "waiting";
|
|
87
|
+
sendButton.disabled = agentPresence === "working";
|
|
88
|
+
if (presenceBanner) presenceBanner.hidden = agentPresence !== "waiting";
|
|
87
89
|
|
|
88
|
-
if (
|
|
90
|
+
if (agentPresence !== "working") {
|
|
89
91
|
if (workingBubble) workingBubble.remove();
|
|
90
92
|
workingBubble = null;
|
|
91
93
|
return;
|
|
@@ -111,7 +113,7 @@ function postToFrame(message) {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
function sendQueued() {
|
|
114
|
-
if (
|
|
116
|
+
if (agentPresence === "working") return;
|
|
115
117
|
|
|
116
118
|
const text = chatInput.value.trim();
|
|
117
119
|
if (text) {
|
|
@@ -128,7 +130,7 @@ function sendQueued() {
|
|
|
128
130
|
async function submitQueued() {
|
|
129
131
|
const prompts = queued.splice(0, queued.length);
|
|
130
132
|
render();
|
|
131
|
-
|
|
133
|
+
if (agentPresence === "listening") setAgentPresence("working");
|
|
132
134
|
await fetch("/api/" + key + "/prompts", {
|
|
133
135
|
method: "POST",
|
|
134
136
|
headers: { "content-type": "application/json" },
|
|
@@ -214,8 +216,8 @@ events.addEventListener("reload", () => {
|
|
|
214
216
|
events.addEventListener("chrome-reload", () => reloadAfterServerRestart());
|
|
215
217
|
events.addEventListener("agent-reply", (event) => addChat("agent", JSON.parse(event.data).text));
|
|
216
218
|
events.addEventListener("chat-sync", (event) => syncChat(JSON.parse(event.data).chat || []));
|
|
217
|
-
events.addEventListener("agent-
|
|
219
|
+
events.addEventListener("agent-presence", (event) => setAgentPresence(JSON.parse(event.data).state));
|
|
218
220
|
|
|
219
221
|
render();
|
|
220
222
|
initialChat.forEach((item) => addChat(item.role, item.text));
|
|
221
|
-
|
|
223
|
+
setAgentPresence("waiting");
|
package/dist/chrome.css
CHANGED
|
@@ -359,6 +359,19 @@ body.lavish {
|
|
|
359
359
|
flex-shrink: 0;
|
|
360
360
|
box-sizing: border-box;
|
|
361
361
|
}
|
|
362
|
+
.presence-banner {
|
|
363
|
+
border: 1px solid var(--border);
|
|
364
|
+
border-radius: var(--radius-sm);
|
|
365
|
+
background: var(--bg-elevated);
|
|
366
|
+
color: var(--fg-muted);
|
|
367
|
+
font-size: 11px;
|
|
368
|
+
font-weight: var(--w-medium);
|
|
369
|
+
line-height: 1.4;
|
|
370
|
+
padding: 8px 10px;
|
|
371
|
+
}
|
|
372
|
+
.presence-banner[hidden] {
|
|
373
|
+
display: none;
|
|
374
|
+
}
|
|
362
375
|
.annotation-pills {
|
|
363
376
|
display: flex;
|
|
364
377
|
flex-wrap: wrap;
|
package/dist/cli.mjs
CHANGED
|
@@ -1187,6 +1187,7 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
|
|
|
1187
1187
|
const events = new EventEmitter();
|
|
1188
1188
|
const watchers = /* @__PURE__ */ new Map();
|
|
1189
1189
|
const activePolls = /* @__PURE__ */ new Map();
|
|
1190
|
+
const deliveredFeedback = /* @__PURE__ */ new Set();
|
|
1190
1191
|
const sseClients = /* @__PURE__ */ new Set();
|
|
1191
1192
|
app.use(express.json({ limit: "2mb" }));
|
|
1192
1193
|
app.get("/health", (req, res) => {
|
|
@@ -1205,7 +1206,11 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
|
|
|
1205
1206
|
const file = await canonicalFile(req.body.file);
|
|
1206
1207
|
const key = sessionKey(file);
|
|
1207
1208
|
const url = `http://localhost:${port}/session/${key}`;
|
|
1209
|
+
const existing = await store.findByKey(key);
|
|
1208
1210
|
const session = await store.upsertSession(file, url);
|
|
1211
|
+
if (existing?.status === "ended") {
|
|
1212
|
+
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
1213
|
+
}
|
|
1209
1214
|
watchSession(session, watchers, events);
|
|
1210
1215
|
res.json({ key, file, url, status: "opened" });
|
|
1211
1216
|
} catch (error) {
|
|
@@ -1219,29 +1224,38 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
|
|
|
1219
1224
|
const timeoutMs = req.query.timeoutMs === void 0 ? null : Math.max(0, Math.min(Number(req.query.timeoutMs || 0), 2147483647));
|
|
1220
1225
|
const immediate = await store.takeFeedback(key);
|
|
1221
1226
|
if (immediate.status !== "waiting") {
|
|
1227
|
+
if (immediate.status === "feedback") markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
|
|
1222
1228
|
res.json(immediate);
|
|
1223
1229
|
return;
|
|
1224
1230
|
}
|
|
1225
|
-
setPollActive(key, activePolls, events, true);
|
|
1226
|
-
const timer = timeoutMs === null ? null : setTimeout(
|
|
1227
|
-
cleanup();
|
|
1228
|
-
res.json(await store.takeFeedback(key));
|
|
1229
|
-
}, timeoutMs);
|
|
1230
|
-
const onFeedback = async (changedKey) => {
|
|
1231
|
-
if (changedKey !== key || res.headersSent) {
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
cleanup();
|
|
1235
|
-
res.json(await store.takeFeedback(key));
|
|
1236
|
-
};
|
|
1231
|
+
setPollActive(key, activePolls, deliveredFeedback, events, true);
|
|
1232
|
+
const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(next), timeoutMs);
|
|
1237
1233
|
let cleaned = false;
|
|
1234
|
+
let responding = false;
|
|
1238
1235
|
const cleanup = () => {
|
|
1239
1236
|
if (cleaned) return;
|
|
1240
1237
|
cleaned = true;
|
|
1241
1238
|
if (timer) clearTimeout(timer);
|
|
1242
1239
|
events.off("feedback", onFeedback);
|
|
1243
1240
|
events.off("ended", onFeedback);
|
|
1244
|
-
setPollActive(key, activePolls, events, false);
|
|
1241
|
+
setPollActive(key, activePolls, deliveredFeedback, events, false);
|
|
1242
|
+
};
|
|
1243
|
+
const respond = async () => {
|
|
1244
|
+
if (responding || res.headersSent) return;
|
|
1245
|
+
responding = true;
|
|
1246
|
+
try {
|
|
1247
|
+
const result = await store.takeFeedback(key);
|
|
1248
|
+
if (result.status === "feedback") markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
|
|
1249
|
+
res.json(result);
|
|
1250
|
+
} finally {
|
|
1251
|
+
cleanup();
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
const onFeedback = (changedKey) => {
|
|
1255
|
+
if (changedKey !== key || res.headersSent) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
respond().catch(next);
|
|
1245
1259
|
};
|
|
1246
1260
|
events.on("feedback", onFeedback);
|
|
1247
1261
|
events.on("ended", onFeedback);
|
|
@@ -1266,6 +1280,7 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
|
|
|
1266
1280
|
app.post("/api/:key/end", async (req, res, next) => {
|
|
1267
1281
|
try {
|
|
1268
1282
|
await store.endSession(req.params.key);
|
|
1283
|
+
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
1269
1284
|
events.emit("ended", req.params.key);
|
|
1270
1285
|
res.json({ status: "ended" });
|
|
1271
1286
|
} catch (error) {
|
|
@@ -1291,6 +1306,7 @@ async function serve({ port, stateFile: stateFile2, version = "" }) {
|
|
|
1291
1306
|
const file = await canonicalFile(req.body.file);
|
|
1292
1307
|
const key = sessionKey(file);
|
|
1293
1308
|
await store.endSession(key);
|
|
1309
|
+
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
1294
1310
|
events.emit("ended", key);
|
|
1295
1311
|
res.json({ status: "ended" });
|
|
1296
1312
|
} catch (error) {
|
|
@@ -1369,10 +1385,10 @@ data: ${JSON.stringify({ text })}
|
|
|
1369
1385
|
`);
|
|
1370
1386
|
}
|
|
1371
1387
|
};
|
|
1372
|
-
const
|
|
1388
|
+
const sendPresence = (key, state) => {
|
|
1373
1389
|
if (key === req.params.key) {
|
|
1374
|
-
res.write(`event: agent-
|
|
1375
|
-
data: ${JSON.stringify({
|
|
1390
|
+
res.write(`event: agent-presence
|
|
1391
|
+
data: ${JSON.stringify({ state })}
|
|
1376
1392
|
|
|
1377
1393
|
`);
|
|
1378
1394
|
}
|
|
@@ -1381,18 +1397,20 @@ data: ${JSON.stringify({ working })}
|
|
|
1381
1397
|
data: ${JSON.stringify({ chat: session?.chat || [] })}
|
|
1382
1398
|
|
|
1383
1399
|
`);
|
|
1384
|
-
res.write(
|
|
1385
|
-
|
|
1400
|
+
res.write(
|
|
1401
|
+
`event: agent-presence
|
|
1402
|
+
data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, deliveredFeedback) })}
|
|
1386
1403
|
|
|
1387
|
-
`
|
|
1404
|
+
`
|
|
1405
|
+
);
|
|
1388
1406
|
events.on("reload", sendReload);
|
|
1389
1407
|
events.on("agent-reply", sendAgentReply);
|
|
1390
|
-
events.on("agent-
|
|
1408
|
+
events.on("agent-presence", sendPresence);
|
|
1391
1409
|
req.on("close", () => {
|
|
1392
1410
|
sseClients.delete(res);
|
|
1393
1411
|
events.off("reload", sendReload);
|
|
1394
1412
|
events.off("agent-reply", sendAgentReply);
|
|
1395
|
-
events.off("agent-
|
|
1413
|
+
events.off("agent-presence", sendPresence);
|
|
1396
1414
|
});
|
|
1397
1415
|
} catch (error) {
|
|
1398
1416
|
next(error);
|
|
@@ -1497,7 +1515,8 @@ function watchSession(session, watchers, events) {
|
|
|
1497
1515
|
});
|
|
1498
1516
|
watchers.set(session.key, watcher);
|
|
1499
1517
|
}
|
|
1500
|
-
function setPollActive(key, activePolls, events, active) {
|
|
1518
|
+
function setPollActive(key, activePolls, deliveredFeedback, events, active) {
|
|
1519
|
+
const previousPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1501
1520
|
const count = activePolls.get(key) || 0;
|
|
1502
1521
|
const nextCount = active ? count + 1 : Math.max(0, count - 1);
|
|
1503
1522
|
if (nextCount === count) return;
|
|
@@ -1505,9 +1524,31 @@ function setPollActive(key, activePolls, events, active) {
|
|
|
1505
1524
|
activePolls.delete(key);
|
|
1506
1525
|
} else {
|
|
1507
1526
|
activePolls.set(key, nextCount);
|
|
1527
|
+
deliveredFeedback.delete(key);
|
|
1508
1528
|
}
|
|
1509
|
-
|
|
1510
|
-
events.emit("agent-
|
|
1529
|
+
const nextPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1530
|
+
if (nextPresence !== previousPresence) events.emit("agent-presence", key, nextPresence);
|
|
1531
|
+
}
|
|
1532
|
+
function markFeedbackDelivered(key, activePolls, deliveredFeedback, events) {
|
|
1533
|
+
const previousPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1534
|
+
deliveredFeedback.add(key);
|
|
1535
|
+
const nextPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1536
|
+
if (nextPresence !== previousPresence) {
|
|
1537
|
+
events.emit("agent-presence", key, nextPresence);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function clearFeedbackDelivery(key, activePolls, deliveredFeedback, events) {
|
|
1541
|
+
const previousPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1542
|
+
deliveredFeedback.delete(key);
|
|
1543
|
+
const nextPresence = computePresence(key, activePolls, deliveredFeedback);
|
|
1544
|
+
if (nextPresence !== previousPresence) {
|
|
1545
|
+
events.emit("agent-presence", key, nextPresence);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function computePresence(key, activePolls, deliveredFeedback) {
|
|
1549
|
+
if (activePolls.has(key)) return "listening";
|
|
1550
|
+
if (deliveredFeedback.has(key)) return "working";
|
|
1551
|
+
return "waiting";
|
|
1511
1552
|
}
|
|
1512
1553
|
function createChromeHtml(session) {
|
|
1513
1554
|
const fileInputSize = Math.max(1, session.file.length);
|
|
@@ -1522,7 +1563,7 @@ function createChromeHtml(session) {
|
|
|
1522
1563
|
</head>
|
|
1523
1564
|
<body class="lavish">
|
|
1524
1565
|
<div class="bar"><div class="brand"><span class="brand-mark">Lavish</span><span class="brand-support">Editor</span></div><div class="divider" aria-hidden="true"></div><div class="file-wrap" title="${escapeHtml(session.file)}"><input class="file-input" id="filePath" readonly size="${fileInputSize}" value="${escapeHtml(session.file)}"><button class="copy-button" id="copyPath" type="button">Copy Path</button></div><button class="button secondary annotation-on" id="annotation">Annotation: On</button><button class="button danger" id="end">End Session</button></div>
|
|
1525
|
-
<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="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>
|
|
1566
|
+
<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>
|
|
1526
1567
|
<script id="lavish-session" type="application/json">${sessionJson}</script>
|
|
1527
1568
|
<script src="/chrome-client.js"></script>
|
|
1528
1569
|
</body>
|
|
@@ -1717,7 +1758,7 @@ function normalizePagePath(path5) {
|
|
|
1717
1758
|
// src/cli.js
|
|
1718
1759
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "server", "playbook", "design"]);
|
|
1719
1760
|
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`.";
|
|
1720
|
-
var VERSION = "0.1.
|
|
1761
|
+
var VERSION = "0.1.9";
|
|
1721
1762
|
async function run(argv) {
|
|
1722
1763
|
await ensureStateDir();
|
|
1723
1764
|
const normalizedArgv = normalizeArgv(argv);
|