lavish-axi 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,10 +155,14 @@ pnpm link
155
155
  - **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
156
156
  For reversible choices, let option clicks update local state, then queue exactly one final answer from a per-question submit or Queue answer button with `window.lavish.queuePrompt()`.
157
157
  Mark only custom (non-native) clickable elements with `data-lavish-action` so Lavish does not annotate them, and use `data-lavish-question` or `queueKey` when pre-send updates for the same question should replace each other.
158
- The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which delivers the prompts before ending the session.
158
+ The browser chrome keeps editing actions in the overflow menu (copy path, reload artifact, copy DOM snapshot, export standalone HTML, publish link, end session) and can submit queued prompts with **Send & end session**, which sends the prompts and user-ended attribution together.
159
159
  - **Keyboard shortcuts** - In the chrome composer, Enter sends queued prompts and Shift+Enter inserts a newline.
160
160
  In the annotation card, Enter queues the annotation, Shift+Enter inserts a newline, and Ctrl+Enter (Cmd+Enter on macOS) queues it and sends all queued prompts immediately.
161
161
  - **Agent presence** - The browser shows when no agent is listening, keeps queued feedback and fresh layout warnings for the next successful `lavish-axi poll` send even across reloads, and only blocks human sends while the agent is working on delivered feedback. The no-timeout poll writes an immediate stderr banner and periodic stderr heartbeats while stdout stays reserved for the final response; if the poll is interrupted or times out, re-run it because queued feedback is never lost.
162
+ - **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.
163
+ 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.
164
+ Agent-initiated ends keep reopening normally, same as before.
165
+ `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.
162
166
  - **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
163
167
  - **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.
164
168
  Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
@@ -171,9 +175,9 @@ pnpm link
171
175
  | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
172
176
  | `lavish-axi` | Show current sessions and usage guidance. |
173
177
  | `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
174
- | `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. |
175
- | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. |
176
- | `lavish-axi end <html-file>` | End a session. |
178
+ | `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
179
+ | `lavish-axi poll <html-file>` | Long-poll until the user sends feedback, ends the session, or the browser reports fresh `layout_warnings`; leave no-timeout polls running, or re-run them if interrupted. On `status: ended`, stop polling and do not reopen uninvited. |
180
+ | `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later. |
177
181
  | `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links. |
178
182
  | `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default, and `--password` makes viewers enter the password before viewing. |
179
183
  | `lavish-axi stop` | Shut down the background server. |
@@ -192,6 +196,7 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
192
196
  | ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
193
197
  | `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
194
198
  | `lavish-axi <html-file>` | `--no-gate` | Skip the open-time layout curtain for this browser open. |
199
+ | `lavish-axi <html-file>` | `--reopen` | Reopen a session the user explicitly ended from the browser; without it, a plain open refuses and explains why instead of reopening uninvited. |
195
200
  | `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
196
201
  | `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
197
202
  | `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private; viewers must supply the password. |
@@ -328,21 +328,26 @@ async function submitQueued() {
328
328
  submitQueuedAgain = false;
329
329
  if (!succeeded) {
330
330
  endAfterSubmit = false;
331
- } else if (shouldSubmitAgain && queued.length) {
332
- submitQueued();
333
- } else if (endAfterSubmit) {
334
- endAfterSubmit = false;
335
- await endSession();
331
+ } else if (!ended && shouldSubmitAgain) {
332
+ if (queued.length) {
333
+ submitQueued();
334
+ } else if (endAfterSubmit) {
335
+ endAfterSubmit = false;
336
+ endSession();
337
+ }
336
338
  }
337
339
  }
338
340
  }
339
341
 
340
342
  async function submitQueuedOnce() {
341
343
  const prompts = queued.slice();
344
+ const shouldEndSession = endAfterSubmit;
345
+ const body = { prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot };
346
+ if (shouldEndSession) body.endSession = true;
342
347
  const response = await fetch("/api/" + key + "/prompts", {
343
348
  method: "POST",
344
349
  headers: { "content-type": "application/json" },
345
- body: JSON.stringify({ prompts: prompts.map(stripInternalPromptFields), domSnapshot: pendingSnapshot }),
350
+ body: JSON.stringify(body),
346
351
  });
347
352
  if (!response.ok) throw new Error("failed to submit queued prompts");
348
353
  for (const prompt of prompts) {
@@ -351,6 +356,11 @@ async function submitQueuedOnce() {
351
356
  }
352
357
  persistQueuedPrompts();
353
358
  render();
359
+ if (shouldEndSession) {
360
+ endAfterSubmit = false;
361
+ markSessionEnded();
362
+ return;
363
+ }
354
364
  if (agentPresence === "listening") setAgentPresence("working");
355
365
  }
356
366
 
@@ -474,6 +484,11 @@ async function endSession() {
474
484
  if (ended) return;
475
485
  const response = await fetch("/api/" + key + "/end", { method: "POST" });
476
486
  if (!response.ok) throw new Error("failed to end session");
487
+ markSessionEnded();
488
+ }
489
+
490
+ function markSessionEnded() {
491
+ if (ended) return;
477
492
  ended = true;
478
493
  closeMenus();
479
494
  annotationSwitch.disabled = true;
package/dist/cli.mjs CHANGED
@@ -5027,13 +5027,16 @@ var SessionStore = class {
5027
5027
  return null;
5028
5028
  }
5029
5029
  const prompts = Array.isArray(payload.prompts) ? payload.prompts : [];
5030
+ const shouldEndSession = Boolean(payload.endSession || payload.end_session);
5031
+ const alreadyEnded = session.status === "ended";
5030
5032
  const normalizedPrompts = prompts.map(normalizePrompt);
5031
5033
  const userMessages = normalizedPrompts.filter((prompt) => prompt.tag === "message" && prompt.prompt).map((prompt) => ({ role: "user", text: prompt.prompt, at: (/* @__PURE__ */ new Date()).toISOString() }));
5032
5034
  session.prompts = [...session.prompts || [], ...normalizedPrompts];
5033
5035
  session.chat = [...session.chat || [], ...userMessages];
5034
5036
  session.pending_prompts = session.prompts.length;
5035
5037
  session.dom_snapshot = String(payload.domSnapshot || payload.dom_snapshot || "");
5036
- session.status = "feedback";
5038
+ session.status = shouldEndSession || alreadyEnded ? "ended" : "feedback";
5039
+ if (shouldEndSession) session.ended_by = "user";
5037
5040
  session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
5038
5041
  await this.writeState(state);
5039
5042
  return session;
@@ -5078,14 +5081,18 @@ var SessionStore = class {
5078
5081
  }
5079
5082
  const prompts = session.prompts || [];
5080
5083
  const layoutWarnings = session.layout_warnings || [];
5084
+ const alreadyEnded = session.status === "ended";
5081
5085
  if (prompts.length === 0 && layoutWarnings.length === 0) {
5082
- return session.status === "ended" ? { status: "ended" } : { status: "waiting" };
5086
+ return alreadyEnded ? { status: "ended", ended_by: session.ended_by } : { status: "waiting" };
5083
5087
  }
5084
5088
  const result = {
5085
5089
  status: "feedback",
5086
5090
  dom_snapshot: session.dom_snapshot || "",
5087
5091
  prompts,
5088
- ...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {}
5092
+ ...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
5093
+ // This is the final delivery before the session shows as ended - flag it so the agent
5094
+ // knows not to expect (or force) a reopened browser afterward.
5095
+ ...alreadyEnded ? { session_ended: true, ended_by: session.ended_by } : {}
5089
5096
  };
5090
5097
  session.prompts = [];
5091
5098
  session.layout_warnings = [];
@@ -5096,20 +5103,26 @@ var SessionStore = class {
5096
5103
  for (const warning of layoutWarnings) deliveredKeys.add(layoutWarningKey(warning));
5097
5104
  session.delivered_layout_warning_keys = [...deliveredKeys].slice(-200);
5098
5105
  }
5099
- if (session.status !== "ended") {
5106
+ if (!alreadyEnded) {
5100
5107
  session.status = "open";
5101
5108
  }
5102
5109
  session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
5103
5110
  await this.writeState(state);
5104
5111
  return result;
5105
5112
  }
5106
- async endSession(key) {
5113
+ // `endedBy` distinguishes a human ending review from the browser chrome ("user") from an
5114
+ // agent explicitly closing the loop via `lavish-axi end` ("agent"). Only a user-initiated end
5115
+ // blocks a plain reopen - see `SessionStore` callers in server.js.
5116
+ async endSession(key, endedBy = "agent") {
5107
5117
  const state = await this.readState();
5108
5118
  const session = state.sessions[key];
5109
5119
  if (!session) {
5110
5120
  return null;
5111
5121
  }
5122
+ const existingEndedBy = session.status === "ended" ? session.ended_by : void 0;
5123
+ const nextEndedBy = endedBy === "user" || existingEndedBy === "user" ? "user" : "agent";
5112
5124
  session.status = "ended";
5125
+ session.ended_by = nextEndedBy;
5113
5126
  session.updated_at = (/* @__PURE__ */ new Date()).toISOString();
5114
5127
  await this.writeState(state);
5115
5128
  return session;
@@ -5256,9 +5269,15 @@ async function serve({
5256
5269
  try {
5257
5270
  const file = await canonicalFile(req.body.file);
5258
5271
  const key = sessionKey(file);
5272
+ const reopen = Boolean(req.body.reopen);
5273
+ const existing = await store.findByKey(key);
5274
+ if (existing?.status === "ended" && existing.ended_by === "user" && !reopen) {
5275
+ logEvent?.(`session open blocked (user-ended) key=${key} file=${file}`);
5276
+ res.json({ key, file, url: existing.url, status: "user-ended" });
5277
+ return;
5278
+ }
5259
5279
  const sessionUrl = `http://${hostForUrl(linkHostName)}:${publicPort}/session/${key}`;
5260
5280
  const url = shouldDisableLayoutGateOpen(req.body || {}) ? appendNoGateParam(sessionUrl) : sessionUrl;
5261
- const existing = await store.findByKey(key);
5262
5281
  const session = await store.upsertSession(file, sessionUrl);
5263
5282
  if (existing?.status === "ended") {
5264
5283
  clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
@@ -5344,13 +5363,16 @@ async function serve({
5344
5363
  });
5345
5364
  app.post("/api/:key/prompts", async (req, res, next) => {
5346
5365
  try {
5366
+ const shouldEndSession = Boolean(req.body?.endSession || req.body?.end_session);
5347
5367
  const session = await store.queuePrompts(req.params.key, req.body || {});
5348
5368
  if (!session) {
5349
5369
  res.status(404).json({ error: "session not found" });
5350
5370
  return;
5351
5371
  }
5352
- events.emit("feedback", req.params.key);
5372
+ if (shouldEndSession) clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
5373
+ events.emit(shouldEndSession ? "ended" : "feedback", req.params.key);
5353
5374
  res.json({ status: "queued", pending_prompts: session.pending_prompts });
5375
+ if (shouldEndSession) await shutdownIfNoLiveSessions();
5354
5376
  } catch (error) {
5355
5377
  next(error);
5356
5378
  }
@@ -5372,7 +5394,7 @@ async function serve({
5372
5394
  });
5373
5395
  app.post("/api/:key/end", async (req, res, next) => {
5374
5396
  try {
5375
- await store.endSession(req.params.key);
5397
+ await store.endSession(req.params.key, "user");
5376
5398
  clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
5377
5399
  events.emit("ended", req.params.key);
5378
5400
  res.json({ status: "ended" });
@@ -5459,7 +5481,7 @@ async function serve({
5459
5481
  try {
5460
5482
  const file = await canonicalFile(req.body.file);
5461
5483
  const key = sessionKey(file);
5462
- await store.endSession(key);
5484
+ await store.endSession(key, "agent");
5463
5485
  clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
5464
5486
  events.emit("ended", key);
5465
5487
  res.json({ status: "ended" });
@@ -6125,7 +6147,7 @@ function normalizePagePath(path6) {
6125
6147
  var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup", "export", "share"]);
6126
6148
  var RESERVED = new Set(RESERVED_COMMANDS);
6127
6149
  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`.";
6128
- var VERSION = "0.1.34";
6150
+ var VERSION = "0.1.35";
6129
6151
  async function run(argv) {
6130
6152
  await ensureStateDir();
6131
6153
  const normalizedArgv = normalizeArgv(argv);
@@ -6217,11 +6239,11 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
6217
6239
  ],
6218
6240
  playbooks: listPlaybooks(),
6219
6241
  help: [
6220
- "Run `lavish-axi <html-file>` to open or resume a Lavish Editor session",
6242
+ "Run `lavish-axi <html-file>` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention",
6221
6243
  "Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
6222
6244
  "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",
6223
- "Run `lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost",
6224
- "Run `lavish-axi end <html-file>` to end a session",
6245
+ "Run `lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead",
6246
+ "Run `lavish-axi end <html-file>` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `lavish-axi <html-file>` refuses to reopen it without `--reopen`",
6225
6247
  "Run `lavish-axi export <html-file> [--out <path>]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu",
6226
6248
  "Run `lavish-axi share <html-file> [--password <pw>] [--token <t>]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu",
6227
6249
  "Run `lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)",
@@ -6250,7 +6272,13 @@ function createPlaybookOutput(args) {
6250
6272
  function createOpenOutput({ file, url, status }) {
6251
6273
  return {
6252
6274
  session: { file, url, status },
6253
- next_step: `Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback, ends the session, or the real browser reports layout_warnings from the in-iframe layout audit, and it stays silent the whole time - that is normal, never kill it. If layout_warnings arrive, follow the poll response's next_step: fix and re-check fresh error-severity overflow or clipped-text findings before involving the human, but persistent or low-severity warnings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if the poll still gets killed or times out, just re-run it - queued feedback is never lost. After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show your response in Lavish Editor and wait for more feedback.`
6275
+ next_step: `Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file}\`. This command long-polls until the user sends feedback, ends the session, or the real browser reports layout_warnings from the in-iframe layout audit, and it stays silent the whole time - that is normal, never kill it. If layout_warnings arrive, follow the poll response's next_step: fix and re-check fresh error-severity overflow or clipped-text findings before involving the human, but persistent or low-severity warnings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if the poll still gets killed or times out, just re-run it - queued feedback is never lost. After applying feedback, run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms to show your response in Lavish Editor and wait for more feedback. If the user ends the session, stop polling and do not reopen it by re-running \`lavish-axi ${file}\` unless the user asks for further review or something genuinely important needs their visual attention - deliver routine updates directly in this conversation instead. When reopening is warranted, run \`lavish-axi ${file} --reopen\`.`
6276
+ };
6277
+ }
6278
+ function createUserEndedOpenOutput({ file, url }) {
6279
+ return {
6280
+ session: { file, url, status: "user-ended" },
6281
+ next_step: `The user explicitly ended this Lavish Editor session from the browser, so \`lavish-axi ${file}\` did not reopen it. Do not reopen unless the user asks for further review or something genuinely important needs their visual attention - deliver routine updates directly in this conversation instead. When reopening is warranted, run \`lavish-axi ${file} --reopen\`.`
6254
6282
  };
6255
6283
  }
6256
6284
  async function openCommand(args) {
@@ -6261,8 +6289,12 @@ async function openCommand(args) {
6261
6289
  await assertHtmlFile(file);
6262
6290
  const absolute = await canonicalFile(file);
6263
6291
  const noGate = args.includes("--no-gate");
6292
+ const reopen = args.includes("--reopen");
6264
6293
  const baseUrl = await ensureServer({ forceRestart: shouldForceRestartForLocalBuild(process.argv[1] || "") });
6265
- const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate });
6294
+ const response = await postJson(`${baseUrl}/api/sessions`, { file: absolute, noGate, reopen });
6295
+ if (response.status === "user-ended") {
6296
+ return createUserEndedOpenOutput({ file: absolute, url: response.url });
6297
+ }
6266
6298
  if (shouldOpenBrowser(args, process.env)) {
6267
6299
  try {
6268
6300
  const open = (await import("open")).default;
@@ -6350,24 +6382,40 @@ function createPollOutput({ file, response }) {
6350
6382
  }
6351
6383
  if (response.status === "feedback") {
6352
6384
  const layoutWarnings = Array.isArray(response.layout_warnings) ? response.layout_warnings : [];
6385
+ const sessionEnded = Boolean(response.session_ended);
6386
+ const endedBy = typeof response.ended_by === "string" ? response.ended_by : void 0;
6353
6387
  return {
6354
- session: { file, status: "feedback" },
6388
+ session: {
6389
+ file,
6390
+ status: "feedback",
6391
+ ...sessionEnded ? { session_ended: true, ...endedBy ? { ended_by: endedBy } : {} } : {}
6392
+ },
6355
6393
  dom_snapshot: response.dom_snapshot || "",
6356
6394
  prompts: response.prompts || [],
6357
6395
  ...layoutWarnings.length > 0 ? { layout_warnings: layoutWarnings } : {},
6358
- next_step: createFeedbackNextStep(file, layoutWarnings)
6396
+ next_step: createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy)
6359
6397
  };
6360
6398
  }
6361
6399
  if (response.status === "ended") {
6362
- return { session: { file, status: "ended" } };
6400
+ return {
6401
+ session: { file, status: "ended", ...response.ended_by ? { ended_by: response.ended_by } : {} },
6402
+ next_step: createEndedNextStep(file, response.ended_by)
6403
+ };
6363
6404
  }
6364
6405
  return {
6365
6406
  session: { file, status: response.status || "waiting" },
6366
6407
  next_step: `No user feedback arrived before the optional timeout. Run \`lavish-axi poll ${file}\` without --timeout-ms to wait indefinitely - queued feedback is never lost, so re-running the poll is always safe.`
6367
6408
  };
6368
6409
  }
6369
- function createFeedbackNextStep(file, layoutWarnings) {
6410
+ function createFeedbackNextStep(file, layoutWarnings, sessionEnded, endedBy) {
6370
6411
  const count = layoutWarnings.length;
6412
+ if (sessionEnded) {
6413
+ const layoutNote = count > 0 ? `${count} layout warning${count === 1 ? "" : "s"} arrived alongside this final feedback. ` : "";
6414
+ if (endedBy === "user") {
6415
+ return `${layoutNote}This was the last feedback before the user ended the session. Stop polling ${file} and do not reopen it - deliver any remaining updates directly in this conversation instead. Only run \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
6416
+ }
6417
+ return `${layoutNote}This was the last feedback before the Lavish Editor session ended. Stop polling ${file}. Deliver any remaining updates directly in this conversation, or run \`lavish-axi ${file}\` to open a fresh session if the user needs further visual review.`;
6418
+ }
6371
6419
  const layoutPrefix = count > 0 ? layoutWarningsPrefix(file, layoutWarnings) : `Apply the requested changes to ${file}. `;
6372
6420
  return `${layoutPrefix}Do not respond to the user just yet. Now you must run \`lavish-axi poll ${file} --agent-reply "<message for the user>"\` without --timeout-ms unless the user ended the session. The poll waits silently until the user sends more feedback, ends the session, or reports fresh layout_warnings - never kill it. If your harness limits how long a foreground command may run, run the poll as a background task; if it still gets killed or times out, just re-run it - queued feedback is never lost.`;
6373
6421
  }
@@ -6386,7 +6434,13 @@ function layoutWarningsPrefix(file, layoutWarnings) {
6386
6434
  if (allRepeatOrLowSeverity) {
6387
6435
  return `${count} layout warning${plural} detected, with no fresh error-severity findings - fix any obvious low-severity issue in ${file}, otherwise it is fine to proceed to the human with a note instead of iterating further. `;
6388
6436
  }
6389
- return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then reload or re-open the artifact and re-check before involving the human. `;
6437
+ return `${count} layout warning${plural} detected - fix horizontal overflow or clipped text in ${file}, then re-check in the browser before involving the human. Lavish live-reloads the artifact automatically after you save, so you do not need to re-run \`lavish-axi ${file}\` for this. `;
6438
+ }
6439
+ function createEndedNextStep(file, endedBy) {
6440
+ if (endedBy === "user") {
6441
+ return `The user ended this Lavish Editor session. Stop polling ${file} - do not run \`lavish-axi ${file}\` to reopen it. Deliver any remaining updates directly in this conversation instead. Only reopen with \`lavish-axi ${file} --reopen\` if the user explicitly asks for further review or something genuinely important needs their visual attention.`;
6442
+ }
6443
+ return `This Lavish Editor session for ${file} has ended. Stop polling. Deliver any remaining updates directly in this conversation, or run \`lavish-axi ${file}\` to open a fresh session if the user needs further visual review.`;
6390
6444
  }
6391
6445
  async function endCommand(args) {
6392
6446
  const file = firstPositionalArg(args);
@@ -6910,7 +6964,7 @@ var TOP_LEVEL_HELP = `lavish-axi - Lavish Editor AXI
6910
6964
 
6911
6965
  Usage:
6912
6966
  lavish-axi
6913
- lavish-axi <html-file> [--no-open] [--no-gate]
6967
+ lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
6914
6968
  lavish-axi poll <html-file> [--agent-reply "..."]
6915
6969
  lavish-axi end <html-file>
6916
6970
  lavish-axi export <html-file> [--out <path>]
@@ -6922,21 +6976,21 @@ Usage:
6922
6976
 
6923
6977
  ${DESIGN_SYSTEM_HINT}
6924
6978
 
6925
- Note: poll long-polls indefinitely by default until the user sends feedback, ends the session, or the browser reports fresh layout_warnings, staying silent while it waits - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost.
6979
+ Note: poll long-polls indefinitely by default until the user sends feedback, ends the session, or the browser reports fresh layout_warnings, staying silent while it waits - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When the user ends a session from the browser, stop polling and do not reopen it uninvited - pass --reopen to <html-file> only when the user asks for further review or something important needs their visual attention.
6926
6980
 
6927
6981
  `;
6928
6982
  var COMMAND_HELP = {
6929
- open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate]
6983
+ open: `Usage: lavish-axi <html-file> [--no-open] [--no-gate] [--reopen]
6930
6984
 
6931
- Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open.
6985
+ Open or resume a Lavish Editor review session for an HTML artifact. Use --no-open when you need to ensure the server/session exists without opening another browser window. Use --no-gate to skip the open-time layout curtain for this browser open. If the user explicitly ended the session from the browser, this refuses to reopen it and returns guidance instead - pass --reopen to force it open when the user asks for further review or something important needs their visual attention. Sessions ended by the agent (\`lavish-axi end\`) reopen normally without the flag.
6932
6986
  `,
6933
6987
  poll: `Usage: lavish-axi poll <html-file> [--agent-reply "..."]
6934
6988
 
6935
- This command long-polls indefinitely for queued user prompts and browser-reported layout_warnings, then returns them to the agent. It stays silent while it waits - that is normal, never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if it still gets killed or times out, just re-run it - queued feedback is never lost. Use --agent-reply after applying prior feedback to display your response in Lavish Editor before waiting again.
6989
+ This command long-polls indefinitely for queued user prompts and browser-reported layout_warnings, then returns them to the agent. It stays silent while it waits - that is normal, never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; persistent or low-severity findings may be surfaced with a note when the cause is not obvious. Do not pass --timeout-ms during normal agent use; it is for tests and debugging only. If your harness limits how long a foreground command may run, run the poll as a background task and wait for it to finish; if it still gets killed or times out, just re-run it - queued feedback is never lost. Use --agent-reply after applying prior feedback to display your response in Lavish Editor before waiting again. When status is ended, stop polling and do not reopen the session uninvited - deliver remaining updates directly in this conversation instead.
6936
6990
  `,
6937
6991
  end: `Usage: lavish-axi end <html-file>
6938
6992
 
6939
- End a Lavish Editor session.
6993
+ End a Lavish Editor session as the agent. A session ended this way still reopens normally on the next \`lavish-axi <html-file>\`, unlike a user ending it from the browser, which requires --reopen.
6940
6994
  `,
6941
6995
  export: `Usage: lavish-axi export <html-file> [--out <path>]
6942
6996
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavish-axi",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
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",
@@ -37,6 +37,7 @@ Use lavish-axi when the user asks for a visual artifact, HTML explainer, interac
37
37
  4. If poll returns `layout_warnings`, follow the returned `next_step`: fix and re-check fresh error-severity findings, but proceed with a note instead of looping when every current warning is persistent or low-severity.
38
38
  5. Apply human feedback, then poll again with `--agent-reply "<message>"` to reply in the browser and keep the loop going.
39
39
  6. Run `npx -y lavish-axi end <html-file>` when the review is finished.
40
+ 7. If the user ends the session from the browser instead, `npx -y lavish-axi <html-file>` refuses to reopen it and says so - only pass `--reopen` when the user asks for further review or something genuinely important needs their visual attention. Otherwise deliver remaining updates directly in this conversation.
40
41
 
41
42
  ## Visual guidance
42
43
 
@@ -62,11 +63,11 @@ For flows, architecture, state, or sequence diagrams, do not hand-build boxes-an
62
63
 
63
64
  ## Commands & rules
64
65
 
65
- - Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session
66
+ - Run `npx -y lavish-axi <html-file>` to open or resume a Lavish Editor session. If the user explicitly ended the session from the browser, this refuses to reopen it and explains why instead of reopening uninvited - pass `--reopen` only when the user asks for further review or something important needs their visual attention
66
67
  - Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`
67
68
  - 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
68
- - Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost
69
- - Run `npx -y lavish-axi end <html-file>` to end a session
69
+ - Run `npx -y lavish-axi poll <html-file>` to wait for user feedback or browser-reported layout_warnings. It long-polls and stays silent until the user sends feedback, ends the session, or the real browser reports fresh layout_warnings, so leave it running - never kill it. Fix and re-check fresh error-severity layout_warnings before involving the human; if the poll says every current warning is persistent or low-severity, proceed with a note instead of looping. If your harness limits how long a foreground command may run, run the poll as a background task; if it gets killed or times out anyway, just re-run it - queued feedback is never lost. When it reports the session ended, stop polling and do not reopen it uninvited - deliver remaining updates in this conversation instead
70
+ - Run `npx -y lavish-axi end <html-file>` to end a session as the agent - ending it this way still allows a plain reopen later. When the user ends it from the browser instead, a later `npx -y lavish-axi <html-file>` refuses to reopen it without `--reopen`
70
71
  - Run `npx -y lavish-axi export <html-file> [--out <path>]` to write a portable copy of the artifact - one HTML file with its LOCAL assets inlined - so it opens with no Lavish server and no sibling files. Remote CDN/font references are left as links, so it needs network to render those. Users can also export from the browser chrome's overflow menu
71
72
  - Run `npx -y lavish-axi share <html-file> [--password <pw>] [--token <t>]` to publish the artifact on ht-ml.app (https://ht-ml.app), a third-party hosting service not part of Lavish, and get back a visitable URL. Shares are PUBLIC by default, so anyone with the link can open them. Pass --password to publish a PRIVATE password-protected page; viewers must supply the password to view. Local assets are inlined; remote refs load over the network. It returns the url plus a secret update_key for managing the page later. Use --token or LAVISH_AXI_HTML_APP_TOKEN only when you have an optional bearer token; it is never required. Users can also publish from the browser chrome's overflow menu
72
73
  - 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)