Commit graph

8 commits

Author SHA1 Message Date
b24af478bf
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.
2026-06-15 11:20:57 -04:00
df154b9a56
repl: auto-pause + portal-save when leaving a tab mid-eval, resume on return
Tab switching during a long-running eval used to silently abandon
the calc — output stopped streaming, no snapshot, nothing to come
back to. Now setActiveTab pauses the outgoing tab's eval (and
optionally portal-saves the env), terminates the worker, and on
re-entry hydrates + re-fires the original input.

Pieces:

* serve-coop.py + make serve-repl — dev server that emits
  Cross-Origin-Opener-Policy: same-origin and
  Cross-Origin-Embedder-Policy: require-corp so SharedArrayBuffer
  is constructable in the browser. Same headers production needs.

* C tier eval-loop pause poll — c/eval.c grows lumbda_check_pause(),
  guarded by #ifdef LUMBDA_WASM. Called at the top of leval()'s
  while(1); masked to every 1024th iteration so the polling cost
  stays under noise floor. When the JS-library import
  js_lumbda_pause_requested returns 1, lisp_error("paused")
  longjmps out so module-global env survives intact for the
  portal-snapshot that follows.

* SAB plumbing — main thread allocates new SharedArrayBuffer(4),
  hands it through worker config → runner.setPauseFlag →
  lumbda-c.loader.setPauseFlag → globalThis._lumbdaCPauseFlag.
  Atomics.store / Atomics.load on index 0 is the signalling
  channel. Falls back to null when COOP/COEP isn't isolated, in
  which case pause degrades to a hard worker.terminate().

* autoPauseTab() — on setActiveTab away, snapshots the tier
  (C tier with SAB) or hard-cancels (other tiers / no SAB),
  stashes tab.autoPause = {tier, blob, inputSrc, savedAt},
  terminates the workers so the heap is reclaimed.

* autoResumeTab() — on setActiveTab into a tab with autoPause,
  reboots the tier, hydrates MEMFS, runs (portal-load! ...), then
  re-fires the original input via sendInput so the eval restarts
  from the saved state. Asm + Python paths re-run from scratch
  until their poll sites land.

Also closes two UX papercuts from fox: chip ⇣ export icon bumped
from 0.85em muted to 1em green so it's actually discoverable; the
scope toggle now reads "scope: this tab" / "scope: all tabs" so the
button label describes the state rather than a target.
2026-06-15 09:02:53 -04:00
2b46c1b79e
repl portal: overwrite-guard, rename, export/import, cross-tab restore
Three UX gaps on the portal-bar closed in one pass plus a defensive
fix on the C tier's heap probe:

* Overwrite guard — saving with an existing name asks 'overwrite?'
  with the existing entry's tier + savedAt. Rename via dbl-click on
  the chip label; same overwrite guard applies on rename.

* Export / import — a ⇣ icon on each chip downloads it as
  <name>.portal.json (opaque blob for c/python, replay-inputs for
  asm). A 📁 import button on the portal-bar accepts a .portal.json
  file via hidden <input type="file">; collisions prompt overwrite,
  decline auto-suffixes (baseName-2, -3, …) so importing a 2nd copy
  always lands somewhere.

* Cross-tab restore — a 📂 this tab / 🌐 all tabs toggle switches
  the chip strip between the active tab's checkpoints and every
  tab's. Global chips render as 'name · tabName' with a dashed
  border; click restores the snapshot into the active tab (the
  saved cp is passed through restoreCheckpoint's new sourceCp
  argument so the chip doesn't need a checkpoints[name] match on
  the active tab). Edit/delete are hidden in global mode — the
  user switches to the owning tab to manage chips.

* heapStats defensive — wasm/c/lumbda-c.loader.js now returns null
  when module.HEAPU8 isn't live yet (caught by the heap poll firing
  during the tiny window between tier reboot and Module init), so a
  restore no longer surfaces 'Cannot read properties of undefined
  (reading byteLength)' as a TypeError.

Smoke-tested headlessly: overwrite confirm fires with the expected
message; rename via dblclick swaps the label; ⇣ produces a download
named '<name>.portal.json'; toggle shows both tabs' chips with the
'· tabName' annotation; import round-trips back into the receiving
tab. Zero page errors across the full flow.
2026-06-15 08:14:53 -04:00
d4380c64c7
repl: portal save/resume — vault-backed tier checkpoints
Adds a portal-bar to the REPL between tabbar and transcript: a save
button + chip strip showing all saved checkpoints for the active tab.
Click a chip to restore, click × to delete.

Per-tier strategy:
  * c, python — call the tier's (portal-snapshot! NAME), then read the
    JSON blob out of MEMFS (Emscripten/Pyodide FS) and stash it in the
    encrypted vault entry. Restore reverses: hydrate MEMFS, then
    (portal-load! NAME) merges the bindings into the live env.
  * asm — no portal serializer in the WAT tier yet (would need a
    Cheney-aware walk). Falls back to transcript replay: save snapshots
    every successful prior input, restore reboots the tier and re-evals
    them in order.

Plumbing:
  * Worker bridge: new portal-save / portal-load message kinds wire
    MEMFS reads/writes to the main thread.
  * runner.js exposes portalSave / portalLoad — null when a tier
    hasn't implemented portals (asm stays grey).
  * C tier: replace EM_JS with extern + --js-library for js_lumbda_bend_call
    (EM_JS-generated declaration was unreachable from wasmImports at
    instantiate time, browsers threw "import object field ... not a
    Function"). FS added to EXPORTED_RUNTIME_METHODS so JS can reach
    pyodide.FS / Module.FS for MEMFS I/O.

Smoke-tested all three tiers headlessly: save → chip render → restore
round-trips clean on c / python / asm, zero console errors.
2026-06-15 07:43:07 -04:00
367071ec51
repl: single page scroller — header pinned, everything else flows
Header (logo + tagline) is the only fixed region. Footer removed.
Everything else — tab bar, transcript, prompt — now lives inside one
.repl-stream scroller. A fresh session shows the prompt right under
the tabs near the top; as entries arrive the prompt drifts down with
them. Tabs use a dashed bottom rule instead of a heavy bar so they
read as the start of the stream rather than a separate chrome strip.
2026-06-14 13:21:02 -04:00
eb58cdc33c
repl: terminal-style transcript — drop card chrome
Reads as one continuous stream now. Each entry is just:
  λ> <input>
  <output>   ; tier · NNms

No left border, no boxed cards, no side-column tier label. Output
indents under the prompt (3ch) using monospace ch units. Tier+time
render as a Lisp-comment-style suffix in muted color.

Prompt bar: borderless textarea on the code-bg surface so the input
visually joins the transcript above. Placeholder cut to "(+ 1 2)" —
the surrounding text already explains the semantics.

Multi-line inputs keep prompt continuation marks ("..").
2026-06-14 13:17:17 -04:00
b3bcff5f08
repl: explain PBKDF2 + AES-GCM on the lock-screen modal 2026-06-14 13:12:05 -04:00
d8ffab5ea6
repl: /repl/ page with encrypted multi-tab sessions
Interactive REPL at lumbda.com/repl with:
  - multi-tab sessions (click + to add, × to close, double-click to rename)
  - per-tab tier selector (python/c/asm/all-three race)
  - persistent transcripts encrypted in localStorage via Web Crypto
    (PBKDF2 + AES-GCM, vault id = SHA-256(password || device-salt) —
    same pattern as unsandbox's vault-encryption-design.md, native
    crypto.subtle API instead of CryptoJS)
  - ephemeral mode (skip vault, transcripts vanish on reload)
  - one worker per (tab × tier) — state persists across evals in a tab
  - reboot tier button (terminate this tab's worker, fresh state next eval)
  - cancel button (kills the running worker in active tab)

Home page now links to both /playground/ and /repl/.

Tier state itself does NOT persist across reloads — the transcript does,
but defines/set!/hash-tables vanish with the worker. Portal save/resume
in WAT (deferred) will let a tier session survive close+reopen.
2026-06-14 12:50:35 -04:00