From 3313407ff653a8f642121dcc8f6d47d088e51cf7 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Fri, 11 Sep 2026 17:53:52 -0400 Subject: [PATCH] prompt.ac: a piece should boot the way it boots everywhere else MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep-linking into a piece put six screens on stage before the piece: a flash of the login curtain, the shell's own "booting" curtain, the boot log, a black gap, and only then the work. The corner label came up with them, printed over the log's own header. Most of it was one wrong guess. The boot canvas reads nogap+nolabel in an iframe as kidlisp.com's $code preview, so it drew that variant — the one that prints the piece name top-left — under prompt.ac's label. The shell says shellhtml; take it at its word and the plain text log comes back, the same one a bare aesthetic.computer load shows. The rest is the shell standing down: the route is settled before first paint so the login curtain never flashes (and .auth[hidden] now really hides — display:flex was outranking the UA rule), the shell's own boot curtain is gone because the runtime already has one, and the stage wears white under a light boot log so the gap before first paint isn't black. The label waits for the piece. `ready` fires while the log still covers the frame, so boot.mjs posts ac:boot-hidden when that log comes down and moat hands it to the shell as onLive. White, boot log, piece. Co-Authored-By: Claude Opus 5 (1M context) --- system/netlify/functions/index.mjs | 5 +- system/public/aesthetic.computer/boot.mjs | 5 ++ system/public/html/lib/moat.mjs | 13 ++++ system/public/prompt.ac/index.html | 93 ++++++++++++++--------- 4 files changed, 80 insertions(+), 36 deletions(-) diff --git a/system/netlify/functions/index.mjs b/system/netlify/functions/index.mjs index ee11702435..397c5a995c 100644 --- a/system/netlify/functions/index.mjs +++ b/system/netlify/functions/index.mjs @@ -1360,7 +1360,10 @@ async function fun(event, context) { // Also detect based on nolabel/nogap query params which indicate embedded preview mode var isL5=false;try{isL5=location.pathname.indexOf('.lua')>=0||(window.self!==window.top&&(document.referrer.indexOf('l5.aesthetic')>=0||document.referrer.indexOf('l5.prompt')>=0));}catch(e){isL5=document.referrer.indexOf('l5.')>=0;} var isKidlisp=false;if(!isL5){try{isKidlisp=window.self!==window.top&&(window.parent.location.hostname.indexOf('kidlisp')>=0||document.referrer.indexOf('kidlisp')>=0);}catch(e){isKidlisp=document.referrer.indexOf('kidlisp')>=0;}} - if(!isKidlisp&&!isL5&&window.self!==window.top){if(qs.indexOf('nolabel')>=0&&qs.indexOf('nogap')>=0)isKidlisp=true;} + // nogap+nolabel reads as "someone else's preview chrome" — kidlisp.com's + // embed. An HTML shell (prompt.ac) passes the same pair but says so with + // shellhtml, and wants the plain boot log, not the $code preview. + if(!isKidlisp&&!isL5&&window.self!==window.top&&qs.indexOf('shellhtml')<0){if(qs.indexOf('nolabel')>=0&&qs.indexOf('nogap')>=0)isKidlisp=true;} // Detect $code piece name from URL pathname OR document.referrer (kidlisp.com parent) var klPieceName='';(function(){var p=location.pathname;if(p.length>1){var pn=p.slice(1).split('?')[0].split(':')[0];if(pn.startsWith('$'))pn=pn.slice(1);if(pn&&pn!=='prompt'&&pn!=='kidlisp')klPieceName=pn;}if(!klPieceName){try{var ref=document.referrer;if(ref&&(ref.indexOf('kidlisp.com')>=0||ref.indexOf('kidlisp')>=0)){var rp=new URL(ref).pathname.slice(1).split('?')[0].split(':')[0];if(rp.startsWith('$'))rp=rp.slice(1);if(rp&&rp!=='prompt'&&rp!=='kidlisp'&&rp!=='device')klPieceName=rp;}}catch(e){}}})(); var klSourceLines=[],klSourceReady=false; diff --git a/system/public/aesthetic.computer/boot.mjs b/system/public/aesthetic.computer/boot.mjs index 458a9eb2af..a09020a6c6 100644 --- a/system/public/aesthetic.computer/boot.mjs +++ b/system/public/aesthetic.computer/boot.mjs @@ -332,6 +332,11 @@ function hideBootLog() { if (bc?.hide) { bc.hide(); } + // 🐚 The piece is on screen now. `ready` (disk loaded) fires earlier, while + // the overlay still stands, so a hosting shell that dresses the runtime — + // prompt.ac's corner label — needs this later moment to avoid drawing its + // chrome over the boot log. + window.parent?.postMessage({ type: "ac:boot-hidden" }, "*"); // Safety net: if the disk-loaded path somehow never marked success, getting // far enough to hide the overlay still counts as a boot. markBootSuccess(); diff --git a/system/public/html/lib/moat.mjs b/system/public/html/lib/moat.mjs index becfe10e2c..74cdb9ea26 100644 --- a/system/public/html/lib/moat.mjs +++ b/system/public/html/lib/moat.mjs @@ -114,6 +114,10 @@ export function el(tag, props = {}, ...kids) { // frame.load("clock") → in-place piece swap (no reload) via `load-piece` // frame.onPiece(cb) → fires when the runtime reports a new slug // frame.onReady(cb) → fires once the runtime is booted + interactive +// frame.onLive(cb) → fires once the runtime's boot overlay is down +// and the piece itself is on screen (later than +// onReady — that one lands while the boot log +// still covers the frame) // In-place swap depends on boot.mjs's `load-piece` parent handler; if the // runtime is older, call frame.load(slug, { reload: true }) to fall back to a // full iframe.src navigation. @@ -123,9 +127,11 @@ export class ACFrame { this.origin = origin; this.piece = null; this.ready = false; + this.live = false; this.bridgeReady = false; this._onPiece = []; this._onReady = []; + this._onLive = []; this._onBridgeReady = []; window.addEventListener("message", (e) => { if (e.source !== iframe.contentWindow) return; @@ -145,6 +151,11 @@ export class ACFrame { this.ready = true; this._onReady.forEach((cb) => cb()); } + } else if (d.type === "ac:boot-hidden") { + if (!this.live) { + this.live = true; + this._onLive.forEach((cb) => cb()); + } } else if (d.type === "ac:bridge-ready") { if (!this.bridgeReady) { this.bridgeReady = true; @@ -158,6 +169,7 @@ export class ACFrame { // the fresh runtime. boot(slug = "prompt", params = "nogap=true&nolabel=true") { this.ready = false; + this.live = false; this.bridgeReady = false; this.piece = null; this.iframe.src = `${this.origin}/${slug}?${params}`; @@ -174,6 +186,7 @@ export class ACFrame { } onPiece(cb) { this._onPiece.push(cb); return this; } onReady(cb) { this._onReady.push(cb); return this; } + onLive(cb) { this._onLive.push(cb); return this; } onBridgeReady(cb) { this._onBridgeReady.push(cb); return this; } } diff --git a/system/public/prompt.ac/index.html b/system/public/prompt.ac/index.html index a7f667d1f0..0aacc57144 100644 --- a/system/public/prompt.ac/index.html +++ b/system/public/prompt.ac/index.html @@ -66,8 +66,16 @@ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } html, body { height: 100%; margin: 0; background: var(--bg); } - body { font-family: var(--font); color: var(--text); overflow: hidden; cursor: var(--cursor-precise), auto; } - #cmd { cursor: var(--cursor-precise), text; } + /* The shell is a screen, not a document: dragging across it paints no blue + wash and a long press raises no callout, the way the raster prompt's + canvas behaves. The typed line is the one place text is really text. */ + body { + font-family: var(--font); color: var(--text); overflow: hidden; + cursor: var(--cursor-precise), auto; + -webkit-user-select: none; user-select: none; + -webkit-touch-callout: none; + } + #cmd { cursor: var(--cursor-precise), text; -webkit-user-select: text; user-select: text; } button, .corner { cursor: var(--cursor-active), pointer; } ::selection { background: var(--highlight); color: var(--bg); } @@ -75,7 +83,12 @@ stage stays inert underneath the opaque #screen — never visibility:hidden, because Chrome throttles rendering for hidden cross-origin iframes and the runtime's boot/load loop would stall (same-origin dev masks this). ── */ + /* The stage's ground is bare for the half-second before the runtime's own + first paint lands, so it wears the same paper the boot log will — black + under the dark log, white under the light one — and the boot starts + without a flash. */ .stage { position: absolute; inset: 0; background: #000; } + @media (prefers-color-scheme: light) { .stage { background: #fff; } } iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; } body[data-mode="prompt"] .stage { pointer-events: none; } @@ -194,6 +207,7 @@ login curtain (showLoginCurtain in prompt.mjs): it stands only while the line is deactivated, empty, and quiet (no reply). ─────────────── */ .auth { display: flex; gap: 14px; transition: opacity .35s; } + .auth[hidden] { display: none; } /* `display: flex` above outranks the UA rule */ body.active .auth, body.typing .auth, body.replying .auth, body.submitting .auth { opacity: 0; pointer-events: none; } .auth button { @@ -220,23 +234,32 @@ font-size: clamp(20px, 2.6vw, 30px); line-height: 1.3; text-shadow: 1px 1px 0 rgba(0, 0, 0, .75); } - body[data-mode="piece"] .corner { display: block; } + body[data-mode="piece"].live .corner { display: block; } body[data-mode="piece"] #screen.overlay ~ .corner { display: none; } .corner:hover { color: var(--highlight); } - /* ── Boot loader — only when deep-linking straight into a piece. ────── */ - .loading { - position: absolute; inset: 0; z-index: 5; display: none; - align-items: center; justify-content: center; - color: var(--dim); font-size: 13px; letter-spacing: .5px; pointer-events: none; - transition: opacity .4s; background: var(--bg); - } - .loading.on { display: flex; } - .loading.gone { opacity: 0; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } - + +
@@ -263,7 +286,6 @@
-
booting aesthetic computer…
runtime booting -- 2.51.2