lumbda/wasm/dist-repl/repl.css
russell@unturf.com 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

390 lines
11 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* lumbda repl — grid-only layout. Inherits palette + base from style.css. */
body.repl {
/* Plain document flow — no min-height, no overflow. The page scrolls
* naturally when content exceeds the viewport; the prompt-bar uses
* position: sticky so it stays glued to the bottom of the viewport in
* that case, and sits right under the transcript otherwise. */
}
/* ─── Lock screen overlay ──────────────────────────────────────── */
.lock-screen {
position: fixed; inset: 0; z-index: 1000;
display: grid;
place-items: center;
background: rgba(15, 17, 21, 0.85);
backdrop-filter: blur(4px);
}
.lock-card {
background: var(--pane-bg);
border: 1px solid var(--rule);
border-radius: 6px;
padding: 1.5rem 2rem;
max-width: 28rem;
width: 90%;
display: grid;
gap: 0.4rem;
}
.lock-card h2 {
margin: 0 0 0.1rem;
font-family: 'chunkfive', Georgia, serif;
font-size: 1.4rem;
color: var(--green);
font-weight: normal;
}
.lock-card p {
margin: 0;
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
.lock-card input[type=password] {
width: 100%;
padding: 0.5rem 0.7rem;
margin: 0.4rem 0 0;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--rule);
border-radius: 3px;
font-family: var(--mono);
font-size: 0.95rem;
}
.lock-card input[type=password]:focus {
outline: none;
border-color: var(--green);
}
.lock-row {
display: grid;
grid-template-columns: auto auto;
gap: 0.6rem;
margin-top: 0.5rem;
justify-content: start;
}
.lock-row button {
font-family: var(--mono);
font-size: 0.85em; font-weight: 600;
padding: 0.45rem 1.1rem;
border-radius: 3px;
cursor: pointer;
border: 1px solid var(--green);
}
.lock-row .primary { background: var(--green); color: var(--bg); }
.lock-row .secondary { background: transparent; color: var(--green); }
.lock-row .primary:hover { filter: brightness(1.1); }
.lock-row .secondary:hover { background: var(--code-bg); }
.lock-note {
margin: 0.4rem 0 0;
color: var(--err);
font-size: 0.8rem;
min-height: 1.2em;
}
.lock-explain {
margin: 0.4rem 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.lock-explain summary {
cursor: pointer;
color: var(--green);
user-select: none;
}
.lock-explain summary:hover { text-decoration: underline; }
.lock-explain ul {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
display: grid;
gap: 0.3rem;
}
.lock-explain li { line-height: 1.45; }
.lock-explain code {
background: var(--code-bg);
padding: 0 0.2em;
border-radius: 2px;
font-size: 0.92em;
}
.lock-explain a {
color: var(--green);
text-decoration: none;
}
.lock-explain a:hover { text-decoration: underline; }
.lock-explain strong { color: var(--fg); }
/* ─── Tab bar ──────────────────────────────────────────────────── */
.tabbar {
/* Sticks to the top of the viewport so it stays visible after the
* header scrolls away — keeps the active-session controls reachable
* even after a long transcript pushes them off-screen. */
position: sticky;
top: 0;
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;
gap: 0.3rem;
align-items: center;
padding: 0.4rem 0.4rem;
margin: 0 0 0.4rem;
background: var(--code-bg);
border-bottom: 1px solid var(--rule);
}
.tabs {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 0.3rem;
overflow-x: auto;
}
.tab {
background: var(--pane-bg);
border: 1px solid var(--rule);
padding: 0.25rem 0.6rem;
border-radius: 3px 3px 0 0;
font-size: 0.85em;
cursor: pointer;
display: inline-grid;
grid-template-columns: auto auto;
gap: 0.3rem;
align-items: center;
white-space: nowrap;
}
.tab.active {
background: var(--code-bg);
border-bottom-color: var(--code-bg);
color: var(--green);
}
.tab .tab-close {
opacity: 0.5;
cursor: pointer;
padding: 0 0.2rem;
}
.tab .tab-close:hover { opacity: 1; color: var(--err); }
.tabbar .ghost {
background: transparent;
border: 1px solid var(--rule);
color: var(--muted);
font-family: var(--mono);
font-size: 0.8em;
padding: 0.2rem 0.6rem;
border-radius: 3px;
cursor: pointer;
}
.tabbar .ghost:hover:not(:disabled) {
border-color: var(--green); color: var(--green);
}
.tabbar .ghost:disabled { opacity: 0.4; cursor: default; }
.tabbar .heap-pressure {
color: var(--muted);
font-size: 0.72em;
font-family: var(--mono);
white-space: nowrap;
padding: 0 0.4rem;
}
/* Portal bar — REPL save/resume controls. Sits between tabbar and
* transcript. Hidden when the active tier has no portal support yet
* (the asm-wat tier stays grey). */
.portal-bar {
display: grid;
/* label · save · import · scope · chips-row */
grid-template-columns: auto auto auto auto 1fr;
gap: 0.4rem;
align-items: center;
padding: 0.2rem 0.5rem;
background: var(--code-bg);
border-bottom: 1px solid var(--rule);
font-size: 0.78em;
font-family: var(--mono);
}
.portal-bar .portal-label {
color: var(--muted);
}
.portal-bar .portal-chips {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 0.3rem;
overflow-x: auto;
}
.portal-chips .chip {
display: inline-grid;
/* label · ⇣ download · × delete */
grid-template-columns: auto auto auto;
gap: 0.3rem;
align-items: center;
background: var(--pane-bg);
border: 1px solid var(--rule);
border-radius: 3px;
padding: 0.1rem 0.4rem;
color: var(--green);
cursor: pointer;
white-space: nowrap;
}
.portal-chips .chip:hover { border-color: var(--green); }
.portal-chips .chip-global {
/* Cross-tab snapshots get a muted accent so they read as "borrowed
* from another tab" rather than belonging here. */
border-style: dashed;
opacity: 0.85;
}
.portal-chips .chip .chip-export,
.portal-chips .chip .chip-close {
/* Bumped from 0.85em + muted because fox couldn't find the export
* icon on the chip. Now sized at parity with the label and tinted
* with the chip's accent so the action affordance reads as a
* sibling of the name, not a footnote. */
font-size: 1em;
cursor: pointer;
padding: 0 0.15rem;
}
.portal-chips .chip .chip-export { color: var(--green); }
.portal-chips .chip .chip-export:hover { filter: brightness(1.4); }
.portal-chips .chip .chip-close { color: var(--muted); }
.portal-chips .chip .chip-close:hover { color: var(--err); }
/* ─── Transcript ───────────────────────────────────────────────── */
/* The REPL stream is the page's only scroller. Header stays pinned;
* tabs, transcript, and prompt all flow inside this one scroller so
* fresh sessions show the prompt up near the top and it drifts down
* with each new entry. */
.repl-stream {
background: var(--code-bg);
display: grid;
grid-template-rows: auto auto auto;
align-content: start;
}
.transcript {
/* Same horizontal inset as the active prompt-bar so prior λ> sigils
* line up with the live one. No max-width / margin: auto here — a
* centered transcript on wide viewports would mis-align with the
* left-flush prompt-bar. Long lines wrap inside the entry instead. */
padding: 0.4rem 0.4rem 0;
font-family: var(--mono);
font-size: 0.92em;
line-height: 1.45;
}
.transcript .entry {
margin: 0;
padding: 0;
}
.transcript .entry .prompt-line {
color: var(--green);
white-space: pre-wrap;
word-break: break-word;
}
.transcript .entry .prompt-line .sigil {
display: inline-block;
width: 3ch;
opacity: 0.7;
}
.transcript .entry .tier-output {
display: block;
padding: 0;
color: var(--fg);
}
.transcript .entry .tier-result {
white-space: pre-wrap;
word-break: break-word;
padding-left: 3ch; /* line up under "λ> " */
}
.transcript .entry .tier-meta {
color: var(--muted);
font-size: 0.78em;
margin-left: 0.6em;
user-select: none;
}
.transcript .entry .tier-meta::before {
content: "; "; /* render as a lisp comment */
color: var(--muted);
opacity: 0.6;
}
.transcript .entry.error .tier-result { color: var(--err); }
/* Error line appended after partial streamed output — keeps the
* lines that already showed and tints just the trailing error. */
.transcript .tier-result .err-line { color: var(--err); }
.transcript .entry + .entry { margin-top: 0.4rem; }
/* ─── Prompt bar ───────────────────────────────────────────────── */
.prompt-bar {
/* Flows directly under the last transcript entry — connected, no gap.
* Sticks to the viewport bottom once the page scrolls past it. */
position: sticky;
bottom: 0;
z-index: 50;
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 0.4rem;
/* Baseline-align: sigil glyph sits on the textarea's first-line
* baseline whether the textarea is 1 row or 40. Center alignment
* with top-aligned sigil drifts visibly off the input baseline on
* single-line input; top alignment without baseline drifts the
* sigil ABOVE the input. Baseline tracks the first line in either
* case, which is what the eye expects from a prompt. */
align-items: baseline;
padding: 0.3rem 0.4rem 0.5rem;
background: var(--code-bg);
}
.prompt-bar .prompt-sigil {
color: var(--green);
font-weight: 600;
/* Match textarea font metrics so the baselines actually line up;
* a larger sigil font would push its baseline higher than the
* textarea's by the cap-height difference. */
font-family: var(--mono);
font-size: 0.92em;
line-height: 1.4;
}
.prompt-bar textarea {
resize: none;
background: var(--code-bg);
border: none;
color: var(--fg);
padding: 0.25rem 0;
font-family: var(--mono);
font-size: 0.92em;
/* Pin line-height so multi-line content + sigil baseline alignment
* stay predictable. Browser default for textarea is 'normal' which
* varies per font; matching .prompt-sigil keeps both baselines on
* the same grid line. */
line-height: 1.4;
min-height: 1.5em;
max-height: 30vh;
min-width: 0;
}
.prompt-bar textarea:focus { outline: none; border-color: var(--green); }
.prompt-bar select {
background: var(--code-bg);
border: 1px solid var(--rule);
color: var(--fg);
padding: 0.35rem 0.5rem;
font-family: var(--mono);
font-size: 0.85em;
border-radius: 3px;
}
.prompt-bar button.primary {
background: var(--green);
color: var(--bg);
border: 1px solid var(--green);
padding: 0.4rem 1rem;
font-family: var(--mono);
font-weight: 600;
border-radius: 3px;
cursor: pointer;
}
.prompt-bar button.primary:hover { filter: brightness(1.1); }
.prompt-bar button.primary:disabled { opacity: 0.4; cursor: wait; }
kbd {
font-family: var(--mono);
background: var(--code-bg);
padding: 0.05em 0.3em;
border: 1px solid var(--rule);
border-radius: 2px;
font-size: 0.85em;
}