diff --git a/wasm/app/app.js b/wasm/app/app.js index 1f9d71e..4482392 100644 --- a/wasm/app/app.js +++ b/wasm/app/app.js @@ -72,7 +72,20 @@ function getEditorText() { async function loadCurrentDemo() { const sel = document.querySelector('input[name="program"]:checked').value; const vaultBar = document.getElementById("vault-bar"); - vaultBar.hidden = sel !== "free-form"; + // Vault bar visible whenever the vault gate has been engaged + // (locked or unlocked) — drafts live per-program now, not just + // for free-form, so the lock/unlock affordance stays useful on + // every demo. Hide entirely only if the user explicitly hasn't + // set up a vault yet (vault.exists()) — kept as-is to avoid + // a "first-paint shows lock UI to a fresh customer" surprise. + vaultBar.hidden = sel !== "free-form" && !vaultState.vault; + // If the vault is unlocked and has a saved draft for this + // program, use it; otherwise fall back to the demo's source. + if (vaultState.vault && vaultState.drafts + && typeof vaultState.drafts[sel] === "string") { + setEditorText(vaultState.drafts[sel]); + return; + } const src = await loadDemoSource(sel); setEditorText(src); } @@ -102,18 +115,30 @@ async function unlockVault() { vaultState.vault = null; return; } - vaultState.freeForm = (data && typeof data.freeForm === "string") - ? data.freeForm - : FREE_FORM_DEFAULT; + // Per-program draft map. Back-compat: lift any legacy + // top-level freeForm field into drafts['free-form'] so users + // who already had a free-form draft don't lose it. + vaultState.drafts = (data && data.drafts && typeof data.drafts === "object") + ? Object.assign({}, data.drafts) + : {}; + if (data && typeof data.freeForm === "string" && !vaultState.drafts["free-form"]) { + vaultState.drafts["free-form"] = data.freeForm; + } + vaultState.freeForm = vaultState.drafts["free-form"] || FREE_FORM_DEFAULT; vaultStateEl.textContent = "unlocked"; vaultStateEl.classList.add("unlocked"); vaultUnlockBtn.hidden = true; vaultLockBtn.hidden = false; vaultPwEl.value = ""; vaultPwEl.disabled = true; - // If free-form is the current program, swap the editor in. + // If the currently-selected program has a saved draft, load it. + // Otherwise leave the demo's default source in place. const sel = document.querySelector('input[name="program"]:checked').value; - if (sel === "free-form") setEditorText(vaultState.freeForm); + if (typeof vaultState.drafts[sel] === "string") { + setEditorText(vaultState.drafts[sel]); + } else if (sel === "free-form") { + setEditorText(vaultState.freeForm); + } setVaultNote("ok — edits auto-save"); } catch (e) { setVaultNote("unlock failed: " + e.message, true); @@ -122,6 +147,7 @@ async function unlockVault() { function lockVault() { vaultState.vault = null; + vaultState.drafts = null; vaultState.freeForm = null; if (vaultState.saveTimer) { clearTimeout(vaultState.saveTimer); vaultState.saveTimer = null; } vaultStateEl.textContent = "locked"; @@ -131,21 +157,38 @@ function lockVault() { vaultPwEl.disabled = false; vaultPwEl.value = ""; setVaultNote(""); - // If free-form is current program, swap to the placeholder. - const sel = document.querySelector('input[name="program"]:checked').value; - if (sel === "free-form") setEditorText(FREE_FORM_DEFAULT); + // Reset the editor to whatever the current program ships with + // (drafts only live while the vault is unlocked). + loadCurrentDemo(); } +// Debounced autosave — fires on every editor doc change while the +// vault is unlocked. Saves under drafts[program-name] so each demo's +// edits persist independently. Previously this was free-form-only, +// which surprised users who edited a demo, switched tabs, and came +// back to their original demo source untouched. function scheduleFreeFormSave() { if (!vaultState.vault) return; const sel = document.querySelector('input[name="program"]:checked').value; - if (sel !== "free-form") return; if (vaultState.saveTimer) clearTimeout(vaultState.saveTimer); vaultState.saveTimer = setTimeout(async () => { const text = getEditorText(); - vaultState.freeForm = text; - try { await vaultState.vault.write({ freeForm: text, savedAt: Date.now() }); } - catch (e) { setVaultNote("save failed: " + e.message, true); } + vaultState.drafts = vaultState.drafts || {}; + vaultState.drafts[sel] = text; + // Keep the legacy freeForm field in sync so older builds that + // only know about that field still see the user's free-form + // edits if they re-open the vault from a different tab. + if (sel === "free-form") vaultState.freeForm = text; + try { + await vaultState.vault.write({ + drafts: vaultState.drafts, + freeForm: vaultState.drafts["free-form"] || "", + savedAt: Date.now(), + }); + setVaultNote(`saved ${sel} @ ${new Date().toLocaleTimeString()}`); + } catch (e) { + setVaultNote("save failed: " + e.message, true); + } }, 350); } diff --git a/wasm/app/style.css b/wasm/app/style.css index 01e4413..d2401f8 100644 --- a/wasm/app/style.css +++ b/wasm/app/style.css @@ -350,6 +350,16 @@ header code { white-space: pre; font-family: var(--mono); } +/* Streamed output: each emit_chunk slice that ended with \n becomes + its own
. Block display
+ guarantees vertical stacking regardless of how the browser handles
+ raw \n inside a sequence of text nodes — the bug we kept chasing
+ in firefox where "tick 0\ntick 10000\n..." rendered horizontally
+ even though each text node clearly contained the newline byte. */
+#output .tier-block .stream-line {
+ display: block;
+ white-space: pre;
+}
#output .err { color: var(--err); }
/* ─── Footer ────────────────────────────────────────────────────── */
diff --git a/wasm/c/lumbda-c.loader.js b/wasm/c/lumbda-c.loader.js
index bfde8d3..1e2e8e2 100644
--- a/wasm/c/lumbda-c.loader.js
+++ b/wasm/c/lumbda-c.loader.js
@@ -30,19 +30,13 @@ async function _bootstrap() {
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
- // Temporary debug — log every Module.print invocation so we can
- // see exactly what Emscripten passes us (with/without trailing
- // newline). Remove once the streaming-newline bug is resolved.
- const DEBUG_STREAM = true;
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier print]", JSON.stringify(line));
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier printErr]", JSON.stringify(line));
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
diff --git a/wasm/dist-repl/c/lumbda-c.loader.js b/wasm/dist-repl/c/lumbda-c.loader.js
index bfde8d3..1e2e8e2 100644
--- a/wasm/dist-repl/c/lumbda-c.loader.js
+++ b/wasm/dist-repl/c/lumbda-c.loader.js
@@ -30,19 +30,13 @@ async function _bootstrap() {
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
- // Temporary debug — log every Module.print invocation so we can
- // see exactly what Emscripten passes us (with/without trailing
- // newline). Remove once the streaming-newline bug is resolved.
- const DEBUG_STREAM = true;
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier print]", JSON.stringify(line));
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier printErr]", JSON.stringify(line));
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
diff --git a/www/playground/app.js b/www/playground/app.js
index 1f9d71e..4482392 100644
--- a/www/playground/app.js
+++ b/www/playground/app.js
@@ -72,7 +72,20 @@ function getEditorText() {
async function loadCurrentDemo() {
const sel = document.querySelector('input[name="program"]:checked').value;
const vaultBar = document.getElementById("vault-bar");
- vaultBar.hidden = sel !== "free-form";
+ // Vault bar visible whenever the vault gate has been engaged
+ // (locked or unlocked) — drafts live per-program now, not just
+ // for free-form, so the lock/unlock affordance stays useful on
+ // every demo. Hide entirely only if the user explicitly hasn't
+ // set up a vault yet (vault.exists()) — kept as-is to avoid
+ // a "first-paint shows lock UI to a fresh customer" surprise.
+ vaultBar.hidden = sel !== "free-form" && !vaultState.vault;
+ // If the vault is unlocked and has a saved draft for this
+ // program, use it; otherwise fall back to the demo's source.
+ if (vaultState.vault && vaultState.drafts
+ && typeof vaultState.drafts[sel] === "string") {
+ setEditorText(vaultState.drafts[sel]);
+ return;
+ }
const src = await loadDemoSource(sel);
setEditorText(src);
}
@@ -102,18 +115,30 @@ async function unlockVault() {
vaultState.vault = null;
return;
}
- vaultState.freeForm = (data && typeof data.freeForm === "string")
- ? data.freeForm
- : FREE_FORM_DEFAULT;
+ // Per-program draft map. Back-compat: lift any legacy
+ // top-level freeForm field into drafts['free-form'] so users
+ // who already had a free-form draft don't lose it.
+ vaultState.drafts = (data && data.drafts && typeof data.drafts === "object")
+ ? Object.assign({}, data.drafts)
+ : {};
+ if (data && typeof data.freeForm === "string" && !vaultState.drafts["free-form"]) {
+ vaultState.drafts["free-form"] = data.freeForm;
+ }
+ vaultState.freeForm = vaultState.drafts["free-form"] || FREE_FORM_DEFAULT;
vaultStateEl.textContent = "unlocked";
vaultStateEl.classList.add("unlocked");
vaultUnlockBtn.hidden = true;
vaultLockBtn.hidden = false;
vaultPwEl.value = "";
vaultPwEl.disabled = true;
- // If free-form is the current program, swap the editor in.
+ // If the currently-selected program has a saved draft, load it.
+ // Otherwise leave the demo's default source in place.
const sel = document.querySelector('input[name="program"]:checked').value;
- if (sel === "free-form") setEditorText(vaultState.freeForm);
+ if (typeof vaultState.drafts[sel] === "string") {
+ setEditorText(vaultState.drafts[sel]);
+ } else if (sel === "free-form") {
+ setEditorText(vaultState.freeForm);
+ }
setVaultNote("ok — edits auto-save");
} catch (e) {
setVaultNote("unlock failed: " + e.message, true);
@@ -122,6 +147,7 @@ async function unlockVault() {
function lockVault() {
vaultState.vault = null;
+ vaultState.drafts = null;
vaultState.freeForm = null;
if (vaultState.saveTimer) { clearTimeout(vaultState.saveTimer); vaultState.saveTimer = null; }
vaultStateEl.textContent = "locked";
@@ -131,21 +157,38 @@ function lockVault() {
vaultPwEl.disabled = false;
vaultPwEl.value = "";
setVaultNote("");
- // If free-form is current program, swap to the placeholder.
- const sel = document.querySelector('input[name="program"]:checked').value;
- if (sel === "free-form") setEditorText(FREE_FORM_DEFAULT);
+ // Reset the editor to whatever the current program ships with
+ // (drafts only live while the vault is unlocked).
+ loadCurrentDemo();
}
+// Debounced autosave — fires on every editor doc change while the
+// vault is unlocked. Saves under drafts[program-name] so each demo's
+// edits persist independently. Previously this was free-form-only,
+// which surprised users who edited a demo, switched tabs, and came
+// back to their original demo source untouched.
function scheduleFreeFormSave() {
if (!vaultState.vault) return;
const sel = document.querySelector('input[name="program"]:checked').value;
- if (sel !== "free-form") return;
if (vaultState.saveTimer) clearTimeout(vaultState.saveTimer);
vaultState.saveTimer = setTimeout(async () => {
const text = getEditorText();
- vaultState.freeForm = text;
- try { await vaultState.vault.write({ freeForm: text, savedAt: Date.now() }); }
- catch (e) { setVaultNote("save failed: " + e.message, true); }
+ vaultState.drafts = vaultState.drafts || {};
+ vaultState.drafts[sel] = text;
+ // Keep the legacy freeForm field in sync so older builds that
+ // only know about that field still see the user's free-form
+ // edits if they re-open the vault from a different tab.
+ if (sel === "free-form") vaultState.freeForm = text;
+ try {
+ await vaultState.vault.write({
+ drafts: vaultState.drafts,
+ freeForm: vaultState.drafts["free-form"] || "",
+ savedAt: Date.now(),
+ });
+ setVaultNote(`saved ${sel} @ ${new Date().toLocaleTimeString()}`);
+ } catch (e) {
+ setVaultNote("save failed: " + e.message, true);
+ }
}, 350);
}
diff --git a/www/playground/c/lumbda-c.loader.js b/www/playground/c/lumbda-c.loader.js
index bfde8d3..1e2e8e2 100644
--- a/www/playground/c/lumbda-c.loader.js
+++ b/www/playground/c/lumbda-c.loader.js
@@ -30,19 +30,13 @@ async function _bootstrap() {
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
- // Temporary debug — log every Module.print invocation so we can
- // see exactly what Emscripten passes us (with/without trailing
- // newline). Remove once the streaming-newline bug is resolved.
- const DEBUG_STREAM = true;
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier print]", JSON.stringify(line));
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier printErr]", JSON.stringify(line));
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
diff --git a/www/playground/style.css b/www/playground/style.css
index 01e4413..d2401f8 100644
--- a/www/playground/style.css
+++ b/www/playground/style.css
@@ -350,6 +350,16 @@ header code {
white-space: pre;
font-family: var(--mono);
}
+/* Streamed output: each emit_chunk slice that ended with \n becomes
+ its own child of the . Block display
+ guarantees vertical stacking regardless of how the browser handles
+ raw \n inside a sequence of text nodes — the bug we kept chasing
+ in firefox where "tick 0\ntick 10000\n..." rendered horizontally
+ even though each text node clearly contained the newline byte. */
+#output .tier-block .stream-line {
+ display: block;
+ white-space: pre;
+}
#output .err { color: var(--err); }
/* ─── Footer ────────────────────────────────────────────────────── */
diff --git a/www/repl/c/lumbda-c.loader.js b/www/repl/c/lumbda-c.loader.js
index bfde8d3..1e2e8e2 100644
--- a/www/repl/c/lumbda-c.loader.js
+++ b/www/repl/c/lumbda-c.loader.js
@@ -30,19 +30,13 @@ async function _bootstrap() {
let outBuf = [];
let errBuf = [];
let currentOnChunk = null;
- // Temporary debug — log every Module.print invocation so we can
- // see exactly what Emscripten passes us (with/without trailing
- // newline). Remove once the streaming-newline bug is resolved.
- const DEBUG_STREAM = true;
const module = await createLumbdaC({
locateFile: (p) => wasmDir + p,
print: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier print]", JSON.stringify(line));
outBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},
printErr: (line) => {
- if (DEBUG_STREAM) console.log("[c-tier printErr]", JSON.stringify(line));
errBuf.push(line);
if (currentOnChunk) currentOnChunk(line + "\n");
},