lavish-axi 0.1.56 → 0.1.57
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 +19 -6
- package/dist/cli.mjs +52 -17
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -204,6 +204,7 @@ pnpm link
|
|
|
204
204
|
The no-timeout poll always writes an immediate stderr banner so it is visibly not hung; it adds the periodic stderr wait ticks only in an interactive terminal, so when stderr is piped (as under agent harnesses) the captured output carries no tick noise. Stdout always stays reserved for the final response; if the poll is interrupted or times out before feedback arrives, re-run it because feedback remains queued until delivery. Poll delivery consumes the response, so read the complete response before truncating or filtering it.
|
|
205
205
|
Codex-specific guidance keeps that poll attached to the active turn instead of hiding it in a background task, because completed background tasks may not resume the agent.
|
|
206
206
|
- **Session end etiquette** - Lavish tracks who ended a session: a human clicking **End session** (or **Send & end session**) in the browser is a user-initiated end, while `lavish-axi end <html-file>` is agent-initiated.
|
|
207
|
+
When either side ends the session, every open review tab becomes visibly read-only and disables its feedback controls; feedback submitted after the end is refused instead of being accepted without an agent to receive it.
|
|
207
208
|
A plain `lavish-axi <html-file>` after a user-initiated end refuses to reopen the browser and returns guidance instead; pass `--reopen` only when the user asks for further review or something important needs their visual attention.
|
|
208
209
|
Agent-initiated ends keep reopening normally, same as before.
|
|
209
210
|
`lavish-axi poll`'s `ended` response and the `feedback` response for the final batch before an end both carry `next_step` guidance telling the agent to stop polling and deliver remaining updates in chat instead of reopening.
|
package/dist/chrome-client.js
CHANGED
|
@@ -746,16 +746,16 @@ function setSheetOpen(open) {
|
|
|
746
746
|
if (sheetOpen) scrollPanelToBottom();
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
// Re-derives every sheet attribute from the
|
|
750
|
-
//
|
|
751
|
-
//
|
|
749
|
+
// Re-derives every sheet attribute from the phone layout, sheet-open, and session-ended state so a
|
|
750
|
+
// viewport crossing the breakpoint in either direction cannot make an ended panel interactive or
|
|
751
|
+
// leave a closed dock trapping focus.
|
|
752
752
|
function applySheetState() {
|
|
753
753
|
const mobile = isMobileSheet();
|
|
754
754
|
const open = mobile && sheetOpen;
|
|
755
755
|
document.body.classList.toggle("sheet-open", open);
|
|
756
756
|
const docked = mobile && !open;
|
|
757
|
-
panelScroll.inert = docked;
|
|
758
|
-
chatComposer.inert = docked;
|
|
757
|
+
panelScroll.inert = ended || docked;
|
|
758
|
+
chatComposer.inert = ended || docked;
|
|
759
759
|
const activeElement = document.activeElement;
|
|
760
760
|
if (docked && activeElement && (panelScroll.contains(activeElement) || chatComposer.contains(activeElement))) {
|
|
761
761
|
panelToggle.focus();
|
|
@@ -1217,6 +1217,14 @@ async function submitQueuedOnce() {
|
|
|
1217
1217
|
if (!response.ok) {
|
|
1218
1218
|
if (response.status === 409) {
|
|
1219
1219
|
const data = await response.json().catch(() => null);
|
|
1220
|
+
// The session already ended before this batch arrived - most likely this chrome missed the
|
|
1221
|
+
// SSE `ended` event (a dropped connection). Go read-only now instead of leaving Send enabled
|
|
1222
|
+
// for another attempt that will be refused the same way.
|
|
1223
|
+
if (data?.status === "ended") {
|
|
1224
|
+
endAfterSubmit = false;
|
|
1225
|
+
markSessionEnded();
|
|
1226
|
+
return false;
|
|
1227
|
+
}
|
|
1220
1228
|
if (Array.isArray(data?.warnings)) setLayoutWarnings(data.warnings);
|
|
1221
1229
|
endAfterSubmit = false;
|
|
1222
1230
|
return false;
|
|
@@ -1778,6 +1786,7 @@ function markSessionEnded() {
|
|
|
1778
1786
|
ended = true;
|
|
1779
1787
|
cancelArtifactLoadRecovery();
|
|
1780
1788
|
closeMenus();
|
|
1789
|
+
closeShareDialog();
|
|
1781
1790
|
closeWarningsDrawer();
|
|
1782
1791
|
renderWarnings();
|
|
1783
1792
|
closeWhiteboard();
|
|
@@ -1785,7 +1794,7 @@ function markSessionEnded() {
|
|
|
1785
1794
|
moreButton.disabled = true;
|
|
1786
1795
|
chatInput.disabled = true;
|
|
1787
1796
|
updateSendState();
|
|
1788
|
-
|
|
1797
|
+
applySheetState();
|
|
1789
1798
|
if (presenceBanner) presenceBanner.hidden = true;
|
|
1790
1799
|
if (handoffBanner) handoffBanner.hidden = true;
|
|
1791
1800
|
if (outdatedBanner) outdatedBanner.hidden = true;
|
|
@@ -3123,6 +3132,7 @@ events.addEventListener("agent-reply", (event) => {
|
|
|
3123
3132
|
events.addEventListener("chat-sync", (event) => syncChat(JSON.parse(event.data).chat || []));
|
|
3124
3133
|
events.addEventListener("agent-presence", (event) => setAgentPresence(JSON.parse(event.data).state));
|
|
3125
3134
|
events.addEventListener("layout-warnings", (event) => setLayoutWarnings(JSON.parse(event.data).warnings || []));
|
|
3135
|
+
events.addEventListener("ended", () => markSessionEnded());
|
|
3126
3136
|
// A reconnecting stream means this chrome may have missed updates while it was away.
|
|
3127
3137
|
events.addEventListener("open", () => refreshLayoutWarnings());
|
|
3128
3138
|
|
|
@@ -3134,6 +3144,9 @@ renderWarnings();
|
|
|
3134
3144
|
initialChat.forEach((item) => addChat(item.role, item.text));
|
|
3135
3145
|
retiredDrafts.forEach((text) => renderRetiredDraft(text));
|
|
3136
3146
|
setAgentPresence("waiting");
|
|
3147
|
+
// The session already ended before this page (re)loaded, so there is no future SSE `ended` event
|
|
3148
|
+
// to wait for - start read-only instead of looking live until a Send gets silently refused.
|
|
3149
|
+
if (sessionData.initialEnded) markSessionEnded();
|
|
3137
3150
|
|
|
3138
3151
|
// Reaching this line is the only proof that this file parsed and ran to completion. The page it
|
|
3139
3152
|
// bootstraps ships with the layout-gate overlay already covering the artifact, and only this
|
package/dist/cli.mjs
CHANGED
|
@@ -7379,6 +7379,9 @@ var SessionStore = class {
|
|
|
7379
7379
|
const shouldEndSession = Boolean(payload.endSession || payload.end_session);
|
|
7380
7380
|
const restoring = options.restore === true;
|
|
7381
7381
|
const alreadyEnded = session.status === "ended";
|
|
7382
|
+
if (alreadyEnded && !restoring) {
|
|
7383
|
+
return { ended: true, ended_by: session.ended_by };
|
|
7384
|
+
}
|
|
7382
7385
|
const normalized = prompts.map(normalizePrompt);
|
|
7383
7386
|
const normalizedPrompts = normalized.map((entry) => entry.prompt);
|
|
7384
7387
|
const rejected = boundAttachmentRefs(normalized, options);
|
|
@@ -8881,6 +8884,10 @@ async function serve({
|
|
|
8881
8884
|
res.status(404).json({ error: "session not found" });
|
|
8882
8885
|
return;
|
|
8883
8886
|
}
|
|
8887
|
+
if (result.ended) {
|
|
8888
|
+
res.status(409).json({ status: "ended", error: "session already ended", ended_by: result.ended_by });
|
|
8889
|
+
return;
|
|
8890
|
+
}
|
|
8884
8891
|
if (result.rejected) {
|
|
8885
8892
|
res.status(400).json({
|
|
8886
8893
|
error: "some attachments could not be delivered",
|
|
@@ -8904,7 +8911,7 @@ async function serve({
|
|
|
8904
8911
|
await syncOutstandingRepairs(req.params.key);
|
|
8905
8912
|
events.emit("layout-warnings", req.params.key, serializeLayoutWarnings(session.layout_warnings));
|
|
8906
8913
|
}
|
|
8907
|
-
events.emit(shouldEndSession ? "ended" : "feedback", req.params.key);
|
|
8914
|
+
events.emit(shouldEndSession ? "ended" : "feedback", req.params.key, session.ended_by);
|
|
8908
8915
|
res.json({ status: "queued", pending_prompts: session.pending_prompts });
|
|
8909
8916
|
if (shouldEndSession) await shutdownIfNoLiveSessions();
|
|
8910
8917
|
} catch (error) {
|
|
@@ -8991,9 +8998,9 @@ async function serve({
|
|
|
8991
8998
|
});
|
|
8992
8999
|
app.post("/api/:key/end", async (req, res, next) => {
|
|
8993
9000
|
try {
|
|
8994
|
-
await store.endSession(req.params.key, "user");
|
|
9001
|
+
const session = await store.endSession(req.params.key, "user");
|
|
8995
9002
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
8996
|
-
events.emit("ended", req.params.key);
|
|
9003
|
+
events.emit("ended", req.params.key, session?.ended_by);
|
|
8997
9004
|
res.json({ status: "ended" });
|
|
8998
9005
|
await shutdownIfNoLiveSessions();
|
|
8999
9006
|
} catch (error) {
|
|
@@ -9080,9 +9087,9 @@ async function serve({
|
|
|
9080
9087
|
try {
|
|
9081
9088
|
const file = await canonicalFile(req.body.file);
|
|
9082
9089
|
const key = sessionKey(file);
|
|
9083
|
-
await store.endSession(key, "agent");
|
|
9090
|
+
const session = await store.endSession(key, "agent");
|
|
9084
9091
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
9085
|
-
events.emit("ended", key);
|
|
9092
|
+
events.emit("ended", key, session?.ended_by);
|
|
9086
9093
|
res.json({ status: "ended" });
|
|
9087
9094
|
await shutdownIfNoLiveSessions();
|
|
9088
9095
|
} catch (error) {
|
|
@@ -9215,6 +9222,8 @@ async function serve({
|
|
|
9215
9222
|
}
|
|
9216
9223
|
});
|
|
9217
9224
|
app.get("/events/:key", async (req, res, next) => {
|
|
9225
|
+
let cleanup = () => {
|
|
9226
|
+
};
|
|
9218
9227
|
try {
|
|
9219
9228
|
res.writeHead(200, {
|
|
9220
9229
|
"content-type": "text/event-stream",
|
|
@@ -9223,7 +9232,6 @@ async function serve({
|
|
|
9223
9232
|
});
|
|
9224
9233
|
sseClients.set(res, String(req.params.key || ""));
|
|
9225
9234
|
refreshIdleTimer();
|
|
9226
|
-
const session = await store.findByKey(req.params.key);
|
|
9227
9235
|
const sendReload = (key) => {
|
|
9228
9236
|
if (key === req.params.key) {
|
|
9229
9237
|
res.write("event: reload\ndata: {}\n\n");
|
|
@@ -9253,29 +9261,51 @@ data: ${JSON.stringify({ warnings })}
|
|
|
9253
9261
|
`);
|
|
9254
9262
|
}
|
|
9255
9263
|
};
|
|
9256
|
-
|
|
9257
|
-
|
|
9264
|
+
const sendEnded = (key, endedBy) => {
|
|
9265
|
+
if (key === req.params.key) {
|
|
9266
|
+
res.write(`event: ended
|
|
9267
|
+
data: ${JSON.stringify({ ended_by: endedBy || null })}
|
|
9258
9268
|
|
|
9259
9269
|
`);
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, deliveredFeedback) })}
|
|
9263
|
-
|
|
9264
|
-
`
|
|
9265
|
-
);
|
|
9270
|
+
}
|
|
9271
|
+
};
|
|
9266
9272
|
events.on("reload", sendReload);
|
|
9267
9273
|
events.on("agent-reply", sendAgentReply);
|
|
9268
9274
|
events.on("agent-presence", sendPresence);
|
|
9269
9275
|
events.on("layout-warnings", sendLayoutWarnings);
|
|
9270
|
-
|
|
9276
|
+
events.on("ended", sendEnded);
|
|
9277
|
+
let cleanedUp = false;
|
|
9278
|
+
cleanup = () => {
|
|
9279
|
+
if (cleanedUp) return;
|
|
9280
|
+
cleanedUp = true;
|
|
9281
|
+
req.off("close", cleanup);
|
|
9271
9282
|
sseClients.delete(res);
|
|
9272
9283
|
events.off("reload", sendReload);
|
|
9273
9284
|
events.off("agent-reply", sendAgentReply);
|
|
9274
9285
|
events.off("agent-presence", sendPresence);
|
|
9275
9286
|
events.off("layout-warnings", sendLayoutWarnings);
|
|
9287
|
+
events.off("ended", sendEnded);
|
|
9276
9288
|
refreshIdleTimer();
|
|
9277
|
-
}
|
|
9289
|
+
};
|
|
9290
|
+
req.once("close", cleanup);
|
|
9291
|
+
const session = await store.findByKey(req.params.key);
|
|
9292
|
+
if (req.destroyed || res.writableEnded) {
|
|
9293
|
+
cleanup();
|
|
9294
|
+
return;
|
|
9295
|
+
}
|
|
9296
|
+
res.write(`event: chat-sync
|
|
9297
|
+
data: ${JSON.stringify({ chat: session?.chat || [] })}
|
|
9298
|
+
|
|
9299
|
+
`);
|
|
9300
|
+
res.write(
|
|
9301
|
+
`event: agent-presence
|
|
9302
|
+
data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, deliveredFeedback) })}
|
|
9303
|
+
|
|
9304
|
+
`
|
|
9305
|
+
);
|
|
9306
|
+
if (session?.status === "ended") sendEnded(req.params.key, session.ended_by);
|
|
9278
9307
|
} catch (error) {
|
|
9308
|
+
cleanup();
|
|
9279
9309
|
next(error);
|
|
9280
9310
|
}
|
|
9281
9311
|
});
|
|
@@ -10035,6 +10065,11 @@ function createChromeHtml(session, {
|
|
|
10035
10065
|
const sessionJson = jsonScript({
|
|
10036
10066
|
key: session.key,
|
|
10037
10067
|
file: session.file,
|
|
10068
|
+
// A page loaded (or reloaded) after the session already ended has no future SSE `ended`
|
|
10069
|
+
// event to wait for - it must start read-only instead of looking live until the user tries
|
|
10070
|
+
// to send and gets refused (#171).
|
|
10071
|
+
initialEnded: session.status === "ended",
|
|
10072
|
+
initialEndedBy: session.ended_by || null,
|
|
10038
10073
|
initialChat: session.chat || [],
|
|
10039
10074
|
// Bootstrapping the inbox from the server is what makes it survive a browser refresh or a
|
|
10040
10075
|
// reconnect: the chrome never owns warning state, it only renders it.
|
|
@@ -10334,7 +10369,7 @@ var POLL_WAKE_PATH_RULES = Object.freeze([
|
|
|
10334
10369
|
]);
|
|
10335
10370
|
var POLL_SEND_AND_END_RULE = "`Send & End` ends the session. Its final feedback is still delivered once. After that response, polling stops, and the agent must not reopen the session uninvited.";
|
|
10336
10371
|
var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
|
|
10337
|
-
var VERSION = "0.1.
|
|
10372
|
+
var VERSION = "0.1.57";
|
|
10338
10373
|
function detectInvokingAgent(env = process.env) {
|
|
10339
10374
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
10340
10375
|
}
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "lavish-axi",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.57",
|
|
5
5
|
"description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kun Chen",
|