diff --git a/wasm/dist-repl/index.html b/wasm/dist-repl/index.html index 1fcec62..9edf37c 100644 --- a/wasm/dist-repl/index.html +++ b/wasm/dist-repl/index.html @@ -84,6 +84,7 @@ + diff --git a/wasm/dist-repl/repl.css b/wasm/dist-repl/repl.css index 1bbce52..7f83941 100644 --- a/wasm/dist-repl/repl.css +++ b/wasm/dist-repl/repl.css @@ -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; diff --git a/wasm/dist-repl/repl.js b/wasm/dist-repl/repl.js index 0c2de3c..0391338 100644 --- a/wasm/dist-repl/repl.js +++ b/wasm/dist-repl/repl.js @@ -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) => { diff --git a/wasm/repl/index.html b/wasm/repl/index.html index 1fcec62..9edf37c 100644 --- a/wasm/repl/index.html +++ b/wasm/repl/index.html @@ -84,6 +84,7 @@ + diff --git a/wasm/repl/repl.css b/wasm/repl/repl.css index 1bbce52..7f83941 100644 --- a/wasm/repl/repl.css +++ b/wasm/repl/repl.css @@ -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; diff --git a/wasm/repl/repl.js b/wasm/repl/repl.js index 0c2de3c..0391338 100644 --- a/wasm/repl/repl.js +++ b/wasm/repl/repl.js @@ -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) => { diff --git a/www/repl/index.html b/www/repl/index.html index 1fcec62..9edf37c 100644 --- a/www/repl/index.html +++ b/www/repl/index.html @@ -84,6 +84,7 @@ + diff --git a/www/repl/repl.css b/www/repl/repl.css index 1bbce52..7f83941 100644 --- a/www/repl/repl.css +++ b/www/repl/repl.css @@ -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; diff --git a/www/repl/repl.js b/www/repl/repl.js index 0c2de3c..0391338 100644 --- a/www/repl/repl.js +++ b/www/repl/repl.js @@ -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) => {