Repository navigation
Launchpad: the first paint is the final page - #270
Conversation
…ets cached, never wait for GitHub in the shell document (F36 addendum) Opening a hosted Environment's Launchpad showed English chrome without the rail for seconds: the page's first reads (profile, shell document, catalog, entry) each waited for a round trip through the gateway, the shell document awaited gh (up to 5 s) for a setup line the Launchpad page never shows, and every asset was downloaded again on each load because the hosted Launchpad wrapped every answer in no-store. A hosted Launchpad now puts a boot document into the page it serves after the gateway's admission (<script type="application/json" id="lazurio-boot">, lazurio.launchpad-boot.v1): the Folder's language, the lazurio.shell.v1 document without setup, the catalog and the entry's public parts, read from the Folder alone under one short read of its state (250 ms for the lock, 500 ms in all) and escaped for its script element. It writes <html lang> and every data-message text in that language with Bun's HTMLRewriter. Nothing from GitHub, no tool output, never the Machine binding, the auth endpoint, the cookie name or the person's account; on any failure or delay the page is served as before. Recovery mode and a workstation's page, served without the token its reads need, carry none. /.lazurio/shell.json takes setup from a cache read in the background (stale while revalidating after 30 s): absent until the first reading ends, which v1 allows, and dropped by a gh sign-in or sign-out, a Tools reading that changes GitHub's state, a content installation and a profile change. Behind the gateway the page's hashed bundle files, the fonts and the stones keep the inner listener's immutable caching; /.lazurio/shell.js carries an ETag and answers 304 to an unchanged copy, locally too. The page, every data document and anything missing stay no-store; the offline guide's headers (F41) are unchanged. Tests: tests/launchpad-boot.test.ts (the boot document's escaping and contents without secrets, its injection only into the admitted page, the assets' caching, a Folder held by another operation, a slow gh that neither the page nor the shell document waits for, the setup cache, Recovery mode and a workstation's page without it); tests/first-run-shell.test.ts waits for the setup's background reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until its module script chose the route, the page showed every frame at once in English: the Settings navigation in the middle of the column, its header with four actions, "Reading Organizations…". Then the right words without the rail and the column head until the shell document arrived. An inline script at the top of index.html, with no request, names the frame of the path and its Settings section on <html data-frame data-section> as pageFrame in routes.ts does (a test keeps the two equal); the stylesheet shows only that frame, and shell.ts keeps both attributes as the page moves. Nothing shows until ui.ts sets data-ready (the language known, the shell document drawn or known to be missing, the catalog on Apps, the profile on Settings), at the latest a second after it ran; the stylesheet shows the page after two seconds without it, and at once without scripts. The rail and the column head have their places before they are defined, and the brand fonts are declared in the page itself. The page reads its boot document first: it hands the shell document to the elements before it defines them, so the rail and the column head draw in the first paint, and draws the catalog and the entry from it without reading them again. The second shell read after the profile is gone; the entry and the prompt hand-off are asked side by side; the tools' sign-in probes and the content, which may ask GitHub, are read at once on a Settings route and elsewhere once the page is idle. Apps is drawn again only when what it shows changed: no forced redraw per Tools reading, none for an Owner answer that changes nothing, none at the start of a later catalog read. The credential helper is now defined before the page's first drawing, which already reads with it: a direct load of /settings/recovery never asked /api/recovery before (the bundled helper was still undefined there), and the Owner question of a home drawn from the boot document needs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Records why a hosted Launchpad page passed through two wrong states (every frame at once in English, then the right words without the rail and the column head while the shell document waited for GitHub) and the design that removes them: the frame named by an inline script before anything runs and kept equal to routes.ts by a test, the page hidden until it is drawn with a one-second cap and a two-second stylesheet fallback, the hosted boot document (lazurio.launchpad-boot.v1: language, shell document without setup, catalog, the entry's public parts; never secrets, the binding, the account or tool output), the page's requests off the first paint's path, setup served from a cache that never waits for GitHub, and the assets' caching behind the gateway. With the alternatives considered, what does not change (lazurio.shell.v1, interface v1, admission, a workstation's token, Recovery mode) and what is left (an unminified bundle, the account and an Owner's answer arriving after the first paint). Measured in the preview with gh answering after a second: hosted Apps went from the static English page at 0.16 s and the final page at 2.65 s (2.50 s on a later load, 1.16 MB each time) to nothing until the final page at 0.50 s (0.11 s and 101 KB on a later load); Settings → Nástroje 2.60 s to 0.62 s; a workstation's Apps 2.37 s to 0.28 s. launchpad-development.md notes the first paint for whoever changes a route or adds a text to the page, and that the shell document's setup is never waited for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…36 addendum) The shell document no longer waits for GitHub, so its `setup` comes from the last reading. Chat and Automate read the shell document once per load, and a reading that only started with their read left the column head without "what is missing" until the next load: on a new Environment the person would not see that GitHub or the content is missing. The first reading now starts when someone opens the Launchpad's page, and a change that drops it (a gh sign-in or sign-out, a Tools reading that changes GitHub's state, a content installation, a profile change) reads it again at once. Not as the Launchpad starts: its Folder operations need the lock then, and a mutation refuses a held lock at once. A new test shows that opening the page reads it before any shell document is asked for, and the cache test that a forget reads again at once; both fail without the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
@immakermatty Exact-head review of 4b141ba: APPROVED. The hosted boot HTML is constructed only after gateway admission, is no-store, and contains the same admitted shell/catalog/public-entry data without token, binding, auth endpoint, cookie name, account or tool output; JSON escapes script delimiters. The frame/section mapping and CSS reveal fallbacks are covered; the setup cache starts only on page open, refreshes after changes, and does not hold the shell response for GitHub. Hashed assets alone retain immutable caching; shell.js revalidates. In an isolated clone I ran check:static, 23 boot/first-paint/setup tests and 40 adjacent hosted/admission/files/recovery/route tests (all pass); git diff --check clean. Head CI passes; zero unresolved threads at review gate. I did not independently repeat the browser frame sampling.
… failing busy (#272) (#317) Since #270 the shell setup cache starts a background read right after a profile change, under the Folder's operation lock. A person's next change, or a tools change after a sign-in, then found the lock held and answered 400 operation-failed with recoveryMayBeRequired, although it would have succeeded a moment later. The same race failed CI often enough on 2026-10-11 to block merges (main's rerun and PR #316, three times). withFolderOperationLock now waits, at most 3 s, while the holder is this process's own short read (withFolderReadLock counts its holds per state directory until the lock is released). Another process's holder, or this process's own mutation, is still refused at once, so the kernel lock stays exclusive and a mutation from elsewhere keeps the immediate refusal. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Status: ready for review. Head 4b141ba adds the reviewer's fix to the setup line on Chat's first load (point 4 and Verification below).
Why
Opening the Launchpad of a hosted Environment (here a personal Remote Environment with a Czech profile) passed through two wrong states before the final one:
POST /api/profileanswered.<lazurio-rail>, the Environment picker with Chat · Apps · Automate). They draw from/.lazurio/shell.json, which awaitedgh auth statusand the content routes' GitHub reads for asetupline the Launchpad page never shows: seconds.On top of that every asset (≈880 KB of unminified JS, fonts, stones) was downloaded again on every hosted load, because the hosted Launchpad wrapped every answer in
no-store, and the startup ran request chains (entry → prompt hand-off, profile → a second shell read), sign-in probes on every route and about ten full rebuilds of the Apps body.Goal: the first paint is the final layout (language, section, rail, column head), in milliseconds, without needless redraws.
What changes
index.html,routes.ts,shell.ts). A tiny inline script at the top of<head>(no request) writes the frame of the path and its Settings section on<html data-frame data-section>, the same mapping as the newpageFrameinroutes.ts;tests/launchpad-first-paint.test.tsevaluates the script againstpageFramefor 31 paths and fails when they drift. The stylesheet shows only that frame and section (and only the matching header action);shell.tskeeps both attributes as the page moves. The page stays hidden untilui.tssetsdata-ready(language known, shell document drawn or known missing, the catalog on Apps, the profile on Settings), at the latest a second after the script ran; a CSS animation shows it after two seconds should the script never do so, and<noscript>at once. The rail's and the column head's places are reserved (:not(:defined)), and the four@font-facerules are declared in the page itself, byte-equal tofontFaceCss()(tested), so the elements add none.boot.ts,boot-document.ts,server.ts). After the gateway's admission, the HTML answer (only the HTML, never an asset) gets<script type="application/json" id="lazurio-boot">at the end of<head>:{schema: "lazurio.launchpad-boot.v1", locale, shell, catalog, entry}— the Folder's language, the samelazurio.shell.v1document as/.lazurio/shell.jsonbut withoutsetupand computed without any gh/network call (includingofflineGuide, so F41's registration is unchanged), the catalog ofPOST /api/catalogandpublicEntryofGET /api/entry.HTMLRewriteralso writes<html lang>, everydata-messagetext and the catalog's loading line from the same messages catalog, so even the skeleton is in the right language. Read under one short Folder read (≤ 250 ms for the lock, ≤ 500 ms in all); on any error or delay the page is served exactly as before.<, U+2028 and U+2029 are escaped. Never the local token, the Machine binding, the auth endpoint or cookie name, the account document or tool output. Recovery mode injects nothing (it serves the same page with 503 and no readable Folder); a workstation's page, served without its token, gets none.ui.ts,catalog-panel.ts). The page reads#lazurio-bootfirst: locale and copy,provideShell(boot.shell)beforedefineShellElements()(so the rail, the column head and F36's new tab icon draw in the first paint), the catalog and the entry drawn without reading them again. The duplicate/.lazurio/shell.jsonread after the profile is gone (it follows a Folder change only); entry and prompt hand-off are asked side by side;tools.refresh()with its sign-in probes andcontent.refresh()run at once on Settings routes and elsewhere once the page is idle. Apps is rebuilt only when what it shows changed: no forced redraw per Tools reading, none for an Owner answer that changes nothing, none at the start of a later catalog read, none when a re-read entry or shell document is unchanged. The credential helper is now defined before the page's first drawing — this also fixes a pre-existing bug: a direct load of/settings/recoverynever asked/api/recovery(the bundled helper was still undefined at that moment, so the view said it could not read the check)./.lazurio/shell.jsonnever waits for GitHub (also for the forks).setupcomes fromcreateSetupCache: the last reading, re-read in the background after 30 s (stale-while-revalidate), omitted until the first reading ends (optional in v1). The first reading starts when someone opens the Launchpad's page, so Chat and Automate (whose column head reads the shell document once per load) find it when they open; not as the Launchpad starts, because a mutation of the Folder refuses a held lock at once and the start is when other Folder operations run. Dropped and read again at once wheregithub.forget()already ran (gh sign-in/out through the Launchpad), on a Tools reading that changes GitHub's state (remembernow reports a change), when a content installation starts and ends, and on a profile change.server.ts,page.ts). Hashed bundle files keep the inner listener'spublic, max-age=31536000, immutable, fonts and stonespublic, max-age=604800, immutable;/.lazurio/shell.jscarries an ETag and answers 304 to an unchanged copy (locally too); the HTML staysno-store, as do API answers, data documents and missing files. The offline guide's headers (F41) are untouched.What does not change
lazurio.shell.v1(additive only:setupwas already optional) and interface v1 of the elements; the forks need no release.Measurements
scripts/preview-launchpad.ts … cs, its stubghslowed to answer after 1 s as GitHub would, each scenario on a freshly started preview; Chrome 1440×900; hosted runs with 60 ms latency and 25 Mbit/s (CDP emulation), workstation on loopback. The page's state was sampled in every animation frame from the document's start (a Playwright init script), requests from CDP. "Later load" = the same browser 62 s later (past the gh probe's cache).main4dab914)/, first load/, later loadWrong states painted (sampled): before 4 (hosted Apps), 3 (hosted Tools), 5 (workstation); after 0 in every scenario. What still changes after the first paint, by design, with its own answer: the "+ Nový modul" tile (GitHub's Owner answer, ~1 RTT later), the favourites hint/list (the person's account through the gateway relay), and avatars/stones as images load.
Verification
bun run check:static✓ (lint, typecheck, check:public, proof build and smoke).bun test --parallel=4: 1564 pass, 7 skip, 0 fail on the code of this branch; a rerun on the final HEAD gave 1563 pass and 1 fail, the free-port race oflaunchpad-shell-routes.test.ts(EADDRINUSEon a port the test probed and released while another worker bound it), which passes alone 3/3. The known load-sensitiveapplication-environmentflake did not occur.tests/launchpad-first-paint.test.ts(inline script ⇔pageFrameparity on 31 paths; fonts ⇔fontFaceCss; both broken once on purpose and failing),tests/launchpad-boot.test.ts(boot JSON escaping and round trip; builder contents with the shell document minussetup, catalog and only public entry parts, no auth endpoint/cookie/port/token; injection only into the admitted HTML withlang="cs"and Czech words, 401 without admission,/filespage too, assets never rewritten; asset cache headers, shell.js ETag/304; a Folder held by another operation still serves the page at once without boot; a slow gh (1.5 s per call) delays neither the page norshell.json, andsetupappears once read; the setup cache's SWR/forget semantics;rememberchange reporting; a workstation's page without boot; hosted Recovery page served 503 without boot).tests/first-run-shell.test.tsnow waits for the background reading.hosted: opening the page reads what the Environment lacks, so Chat finds it when it opensand the forget step of the setup cache test. A first attempt that read at Launchpad start madea Folder held by another operation…fail on the held lock, which is why the reading waits for a page.bun run check:static✓;bun test --parallel=4: 1565 pass, 7 skip, 0 fail. Independent frame sampling against the personal Czech preview (60 ms, 25 Mbit/s): first load nothing visible until the final page at 0.52 s (rail, column head, Apps, Czech), later load final at 0.11 s with 99 KiB; tools and content requests start after the paint. Checked in the in-app browser: Apps with the first-run tour and the GitHub line, Settings → Nástroje showing only its section,lang="cs", the boot document present./,/settings/general|machine|tools|recovery,/files,/marketplace,/o/example/deals) shows its final frame, language, rail and column head in its first visible frame; in-page moves (Files → gear → Tools → Back → history back) keepdata-frame/data-sectionand the shown frame in step; 390 px narrow layout unchanged; no page errors (only the preview's expected 404/503 answers of the account relay and update pill); a workstation Recovery-mode page renders and reveals at ~0.2 s.Risks
setupinshell.jsonis absent on a fork's load only before anyone opened the Launchpad's page since it started, or within the seconds of a re-reading after a change; that fork shows the line from its next load. A sign-in done outside the Launchpad (a terminalgh auth login) shows up within ~30 s plus one load.<script>: there is no CSP on the page today; should a gateway addscript-src 'self', the page degrades to the catalog frame until the module runs (the boot data block is not a script and is unaffected).Not done / follow-ups
--minifyof the HTML bundle in the release build would be a separate, release-pipeline change.<link rel="preload">for the Latin font: Bun's HTML bundler refuses absolute/.lazurio/…link targets, and the reveal waits for the script anyway (fonts finish first even on a first load), so it would not move the first paint.Commits: server side (boot document, caching, setup cache) · page side (first paint, client boot path, requests) · docs (F36 addendum, launchpad-development notes) · the setup reading on page open and re-reading after a change (4b141ba).
🤖 Generated with Claude Code