repl: ⏸ pause button — manual snapshot mid-eval without leaving the tab

Adds a tabbar button that signals the SAB pause atomic, waits for
the eval to unwind via lisp_error/LispErr("paused"), then drops
into saveCheckpoint so the resulting snapshot lands in the visible
chip strip with a user-chosen name. Workers stay alive so the next
input runs immediately — this is the "I want to inspect/save state
but keep working" path, distinct from autoPauseTab's
"I'm leaving this tab" flow.

The button enables alongside cancel whenever an eval is running and
SAB is available; on plain http.server (no COOP/COEP) it stays
disabled and an alert points at make serve-repl. Asm tier gets a
"not supported on asm tier yet" alert since the WAT interpreter
has no in-eval poll site.

Tabbar grid expanded from 8 to 9 columns; comment updated to track.
This commit is contained in:
russell@unturf.com 2026-06-15 11:20:57 -04:00
parent 6bcc20cb27
commit b24af478bf
No known key found for this signature in database
9 changed files with 111 additions and 12 deletions

View file

@ -84,6 +84,7 @@
<span class="spacer"></span>
<button id="reset-tier" class="ghost" title="reboot the worker for the current tab — state lost">↻ reboot tier</button>
<button id="clear-log" class="ghost" title="clear the transcript">clear log</button>
<button id="pause" class="ghost" disabled title="pause the running eval and snapshot its state into the portal vault — resume via the chip">⏸ pause</button>
<button id="cancel" class="ghost" disabled>cancel</button>
<button id="lock" class="ghost" title="lock vault — passes back to lock screen">lock</button>
</section>

View file

@ -121,10 +121,10 @@ body.repl {
z-index: 40;
display: grid;
/* tabs · new-tab · spacer · heap-pressure (injected by repl.js) ·
* reboot · clear · cancel · lock — 8 cells. Extend if the toolbar
* grows; CSS Grid drops trailing children onto a new row, which is
* how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto;
* reboot · clear · pause · cancel · lock — 9 cells. Extend if the
* toolbar grows; CSS Grid drops trailing children onto a new row,
* which is how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto auto;
gap: 0.3rem;
align-items: center;
padding: 0.4rem 0.4rem;

View file

@ -21,6 +21,7 @@ const newTabBtn = document.getElementById("new-tab");
const resetTierBtn = document.getElementById("reset-tier");
const clearLogBtn = document.getElementById("clear-log");
const cancelBtn = document.getElementById("cancel");
const pauseBtn = document.getElementById("pause");
const lockBtn = document.getElementById("lock");
const replStream = document.getElementById("repl-stream");
@ -671,6 +672,34 @@ async function importCheckpoint(file) {
saveSoon();
}
// Manual pause-and-snapshot — the user wants to inspect or save the
// state of a long-running eval without leaving the tab. Signals the
// SAB pause flag, waits for the in-flight eval to settle, then runs
// saveCheckpoint so the snapshot lands in the visible chip strip
// with a user-chosen name. Workers stay alive so the next input
// runs immediately.
async function pauseAndSnapshot() {
if (!pauseFlagView) {
alert("pause requires COOP/COEP headers — start with `make serve-repl`, or for production set Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp on the deploy");
return;
}
const tab = activeTab();
if (!tab) return;
const tier = tab.tier === "all" ? "c" : tab.tier;
if (tier === "asm") {
alert("manual pause not supported on asm tier yet — use cancel + portal-save when the eval is idle");
return;
}
pauseBtn.disabled = true;
Atomics.store(pauseFlagView, 0, 1);
await waitForPending(tab.id, tier, 5000);
Atomics.store(pauseFlagView, 0, 0);
// Reset activeInput marker so a follow-up tab switch doesn't try
// to "auto-resume" what the user has now explicitly saved.
tab.activeInput = null;
await saveCheckpoint();
}
function deleteCheckpoint(name) {
const tab = activeTab();
if (!tab || !tab.checkpoints) return;
@ -973,6 +1002,7 @@ async function sendInput() {
resetHistory();
sendBtn.disabled = true;
cancelBtn.disabled = false;
pauseBtn.disabled = !pauseFlagView;
renderAll();
saveSoon();
@ -1033,6 +1063,7 @@ async function sendInput() {
if (remaining === 0) {
sendBtn.disabled = false;
cancelBtn.disabled = true;
pauseBtn.disabled = true;
inputEl.focus();
// Eval finished cleanly — drop the activeInput marker so
// a subsequent tab switch doesn't try to resume something
@ -1102,6 +1133,7 @@ clearLogBtn.addEventListener("click", () => {
if (tab) { tab.transcript = []; renderAll(); saveSoon(); }
});
cancelBtn.addEventListener("click", cancelAllPendingInActiveTab);
pauseBtn.addEventListener("click", pauseAndSnapshot);
portalSaveBtn.addEventListener("click", saveCheckpoint);
portalImportBtn.addEventListener("click", () => portalImportFileEl.click());
portalImportFileEl.addEventListener("change", async (e) => {

View file

@ -84,6 +84,7 @@
<span class="spacer"></span>
<button id="reset-tier" class="ghost" title="reboot the worker for the current tab — state lost">↻ reboot tier</button>
<button id="clear-log" class="ghost" title="clear the transcript">clear log</button>
<button id="pause" class="ghost" disabled title="pause the running eval and snapshot its state into the portal vault — resume via the chip">⏸ pause</button>
<button id="cancel" class="ghost" disabled>cancel</button>
<button id="lock" class="ghost" title="lock vault — passes back to lock screen">lock</button>
</section>

View file

@ -121,10 +121,10 @@ body.repl {
z-index: 40;
display: grid;
/* tabs · new-tab · spacer · heap-pressure (injected by repl.js) ·
* reboot · clear · cancel · lock — 8 cells. Extend if the toolbar
* grows; CSS Grid drops trailing children onto a new row, which is
* how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto;
* reboot · clear · pause · cancel · lock — 9 cells. Extend if the
* toolbar grows; CSS Grid drops trailing children onto a new row,
* which is how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto auto;
gap: 0.3rem;
align-items: center;
padding: 0.4rem 0.4rem;

View file

@ -21,6 +21,7 @@ const newTabBtn = document.getElementById("new-tab");
const resetTierBtn = document.getElementById("reset-tier");
const clearLogBtn = document.getElementById("clear-log");
const cancelBtn = document.getElementById("cancel");
const pauseBtn = document.getElementById("pause");
const lockBtn = document.getElementById("lock");
const replStream = document.getElementById("repl-stream");
@ -671,6 +672,34 @@ async function importCheckpoint(file) {
saveSoon();
}
// Manual pause-and-snapshot — the user wants to inspect or save the
// state of a long-running eval without leaving the tab. Signals the
// SAB pause flag, waits for the in-flight eval to settle, then runs
// saveCheckpoint so the snapshot lands in the visible chip strip
// with a user-chosen name. Workers stay alive so the next input
// runs immediately.
async function pauseAndSnapshot() {
if (!pauseFlagView) {
alert("pause requires COOP/COEP headers — start with `make serve-repl`, or for production set Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp on the deploy");
return;
}
const tab = activeTab();
if (!tab) return;
const tier = tab.tier === "all" ? "c" : tab.tier;
if (tier === "asm") {
alert("manual pause not supported on asm tier yet — use cancel + portal-save when the eval is idle");
return;
}
pauseBtn.disabled = true;
Atomics.store(pauseFlagView, 0, 1);
await waitForPending(tab.id, tier, 5000);
Atomics.store(pauseFlagView, 0, 0);
// Reset activeInput marker so a follow-up tab switch doesn't try
// to "auto-resume" what the user has now explicitly saved.
tab.activeInput = null;
await saveCheckpoint();
}
function deleteCheckpoint(name) {
const tab = activeTab();
if (!tab || !tab.checkpoints) return;
@ -973,6 +1002,7 @@ async function sendInput() {
resetHistory();
sendBtn.disabled = true;
cancelBtn.disabled = false;
pauseBtn.disabled = !pauseFlagView;
renderAll();
saveSoon();
@ -1033,6 +1063,7 @@ async function sendInput() {
if (remaining === 0) {
sendBtn.disabled = false;
cancelBtn.disabled = true;
pauseBtn.disabled = true;
inputEl.focus();
// Eval finished cleanly — drop the activeInput marker so
// a subsequent tab switch doesn't try to resume something
@ -1102,6 +1133,7 @@ clearLogBtn.addEventListener("click", () => {
if (tab) { tab.transcript = []; renderAll(); saveSoon(); }
});
cancelBtn.addEventListener("click", cancelAllPendingInActiveTab);
pauseBtn.addEventListener("click", pauseAndSnapshot);
portalSaveBtn.addEventListener("click", saveCheckpoint);
portalImportBtn.addEventListener("click", () => portalImportFileEl.click());
portalImportFileEl.addEventListener("change", async (e) => {

View file

@ -84,6 +84,7 @@
<span class="spacer"></span>
<button id="reset-tier" class="ghost" title="reboot the worker for the current tab — state lost">↻ reboot tier</button>
<button id="clear-log" class="ghost" title="clear the transcript">clear log</button>
<button id="pause" class="ghost" disabled title="pause the running eval and snapshot its state into the portal vault — resume via the chip">⏸ pause</button>
<button id="cancel" class="ghost" disabled>cancel</button>
<button id="lock" class="ghost" title="lock vault — passes back to lock screen">lock</button>
</section>

View file

@ -121,10 +121,10 @@ body.repl {
z-index: 40;
display: grid;
/* tabs · new-tab · spacer · heap-pressure (injected by repl.js) ·
* reboot · clear · cancel · lock — 8 cells. Extend if the toolbar
* grows; CSS Grid drops trailing children onto a new row, which is
* how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto;
* reboot · clear · pause · cancel · lock — 9 cells. Extend if the
* toolbar grows; CSS Grid drops trailing children onto a new row,
* which is how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto auto;
gap: 0.3rem;
align-items: center;
padding: 0.4rem 0.4rem;

View file

@ -21,6 +21,7 @@ const newTabBtn = document.getElementById("new-tab");
const resetTierBtn = document.getElementById("reset-tier");
const clearLogBtn = document.getElementById("clear-log");
const cancelBtn = document.getElementById("cancel");
const pauseBtn = document.getElementById("pause");
const lockBtn = document.getElementById("lock");
const replStream = document.getElementById("repl-stream");
@ -671,6 +672,34 @@ async function importCheckpoint(file) {
saveSoon();
}
// Manual pause-and-snapshot — the user wants to inspect or save the
// state of a long-running eval without leaving the tab. Signals the
// SAB pause flag, waits for the in-flight eval to settle, then runs
// saveCheckpoint so the snapshot lands in the visible chip strip
// with a user-chosen name. Workers stay alive so the next input
// runs immediately.
async function pauseAndSnapshot() {
if (!pauseFlagView) {
alert("pause requires COOP/COEP headers — start with `make serve-repl`, or for production set Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp on the deploy");
return;
}
const tab = activeTab();
if (!tab) return;
const tier = tab.tier === "all" ? "c" : tab.tier;
if (tier === "asm") {
alert("manual pause not supported on asm tier yet — use cancel + portal-save when the eval is idle");
return;
}
pauseBtn.disabled = true;
Atomics.store(pauseFlagView, 0, 1);
await waitForPending(tab.id, tier, 5000);
Atomics.store(pauseFlagView, 0, 0);
// Reset activeInput marker so a follow-up tab switch doesn't try
// to "auto-resume" what the user has now explicitly saved.
tab.activeInput = null;
await saveCheckpoint();
}
function deleteCheckpoint(name) {
const tab = activeTab();
if (!tab || !tab.checkpoints) return;
@ -973,6 +1002,7 @@ async function sendInput() {
resetHistory();
sendBtn.disabled = true;
cancelBtn.disabled = false;
pauseBtn.disabled = !pauseFlagView;
renderAll();
saveSoon();
@ -1033,6 +1063,7 @@ async function sendInput() {
if (remaining === 0) {
sendBtn.disabled = false;
cancelBtn.disabled = true;
pauseBtn.disabled = true;
inputEl.focus();
// Eval finished cleanly — drop the activeInput marker so
// a subsequent tab switch doesn't try to resume something
@ -1102,6 +1133,7 @@ clearLogBtn.addEventListener("click", () => {
if (tab) { tab.transcript = []; renderAll(); saveSoon(); }
});
cancelBtn.addEventListener("click", cancelAllPendingInActiveTab);
pauseBtn.addEventListener("click", pauseAndSnapshot);
portalSaveBtn.addEventListener("click", saveCheckpoint);
portalImportBtn.addEventListener("click", () => portalImportFileEl.click());
portalImportFileEl.addEventListener("change", async (e) => {