feat(chrome): light and system appearance for the editor chrome - #203
feat(chrome): light and system appearance for the editor chrome#203pvinis wants to merge 12 commits into
Conversation
readState rebuilt state from `sessions` alone, so anything else at the top level of state.json was silently lost on the next session write. spread the parsed state instead, and add get/setThemePreference on top of it for a device-wide theme preference under config.theme. without the readState fix that preference would reset itself the moment any session was touched.
device-wide setting stored as config.theme in state.json, so every browser on the machine agrees instead of each one keeping its own localStorage toggle. nothing reads the value yet, that comes next. this is just the command surface, validation, and docs.
the chrome already had a semantic token layer over the raw ramp, but a few surfaces still had literal colors baked into the rule: the three hovers, the three overlay scrims, the layout banner, the danger tint, and the shadow color. no visual change, every new token resolves to exactly the value it replaced. this just puts them in one place so a light theme has something to reassign. .frame stays #fff on purpose. it is the artifact viewport, not chrome, so it must not follow the chrome theme.
createChromeHtml now emits <html data-theme-pref="system|light|dark">, read from the device config on each session request. chrome.css is a render-blocking <link> in <head>, so keying the theme off an attribute means it resolves before first paint. no inline boot script, no flash of the wrong theme, no js. nothing styles off it yet, that's the next commit.
…heme adds a warm paper counterpart to the semantic token layer. the ramp holds both palettes, so only the ~25 semantic assignments are restated for light. `light` pins through :root[data-theme-pref="light"]. `system` resolves inside a prefers-color-scheme media query, so it follows the OS live with no listener and no reload. no js at all, and the stylesheet is render-blocking so both resolve before first paint. light foregrounds are tuned so every pair clears WCAG AA on its own surface, and --accent/--accent-hover go dark brass since --accent-hover is used as link text on paper as well as a button fill. verified in chrome across the matrix: system follows the OS both ways, light and dark stay pinned against the opposite OS setting, and the dark theme resolves to exactly the values it had before.
whiteboardTheme now reads document.documentElement.dataset.themePref, but the fake document in the chrome harness never modelled documentElement, so every whiteboard message path threw and 15 tests failed. a real document always has one, so the stub was the gap.
|
Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch. When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again. Noted for lavish-axi#203 at |
|
Automated reminder: this PR still looks blocked on a rebase or merge conflict fix. If you are still interested, please rebase onto the current base branch, resolve the conflict, and push. If I do not hear back, I may close this as inactive. |
|
Speaking as Kun's firstmate: Holding this. The branch is CONFLICTING with main. #202 is labeled ready-for-pr; landing waits on rebase of the existing PRs (#203 / #204). This is not a merge call while the branch is dirty. |
closes #202
this is one of two PRs for the same feature. only one should land. #204 is the alternative. say which you want and i'll close the other same day.
pick one: the difference is one file
both PRs carry the same four base commits and the same review gate fixes, on top of one differing implementation commit. what actually differs between the two branches is
src/chrome.css, its test, and a one line invariant note. they render pixel identical in both themes, so this is purely an implementation choice.:root[data-theme-pref="light"]plus aprefers-color-schememedia querylight-dark()chrome.cssvs mainlight-dark(), baseline may 2024short version: this one is the conservative pick, no new css features, at the cost of the palette appearing twice. #204 is smaller and has nothing to keep in sync, but needs 2024 css.
on that floor, for what it's worth: there's no
browserslistor@supportsanywhere in the repo,enginesonly pins node, and lavish opens a tab in the developer's own current browser. #201 is an eventsource failure on firefox 151, not a css feature problem.what it does
lavish-axi config theme <system|light|dark>, device wide, drives the editor chrome. defaults tosystem.following your notes on #131
configcommand instate.json, not per browser localStorage. done.also dropped from #131:
src/appearance.js. it shelled out to osascript, the windows registry and gsettings to resolvesystemat cli time, and that only existed to serve the agent guidance half. the chrome runs in a browser, soprefers-color-schemedoes it for free.the chrome theme still resolves in pure css. no injected stylesheet, no boot script, no
matchMedialistener.what i can't claim any more is "zero new javascript" overall, which is what i said when i first opened this. the review gate found two real bugs in the shared commits and fixing them added js. details below. none of it changes how the theme resolves, and all of it is in the shared commits, so #204 carries exactly the same code.
the default
systemmeans a light mode machine gets different looking chrome after upgrading. deliberate, but it is a visible change, so shout if you'd rather it default todarkand stay opt in. one line.how
chrome.cssis a render blocking<link>in<head>, so there's no fouc to defend against and no inline boot script needed.createChromeHtmlemits<html data-theme-pref="...">and the css resolves it:systemgoing through a media query means it follows the os live, nomatchMedialistener.the ramp holds both palettes so only the ~25 semantic assignments are stated twice. there's a test asserting the two blocks stay identical, since that duplication is the only real maintenance cost here.
what the review gate changed
pushed through
no-mistakesper CONTRIBUTING.md. it found two genuine bugs in the shared commits that i'd missed, both worth having:state.jsonhad a second writer.configCommanddid a read modify write straight from the cli process, which breaks the invariantAGENTS.mdleans on ("writes are serialized through the single server process"). concurrent with a server sidequeuePrompts, last writer wins and the other change is silently gone. fixed at the shared boundary:writeStateis now a temp file plus rename, andconfig themeposts to a newPOST /api/configwhen a server is live. it writes directly only when nothing answers/health, and refuses outright rather than writing beside a stale server.whiteboardTheme()read onlyprefers-color-scheme, soconfig theme lighton a dark os gave you a light chrome around a dark excalidraw canvas. it now reads the resolved preference first.that's the new javascript. i also fixed a test harness gap the gate's own fix exposed: the fake document in
test/chrome-client-queue.test.jsnever modelleddocumentElement, so the whiteboard change failed 15 tests there until it did.pnpm run checkis green: 625 tests, 623 pass, 2 skipped (they need chrome).chrome vs artifact
worth being explicit since it surprised me too. the artifact iframe is sandboxed without
allow-same-origin, so it can't see the chrome preference, only the os. so:prefers-color-schemedark mode: follows the oswith the default
systemthat means chrome and artifact now agree, which they never did before: today the chrome is pinned dark, so every light mode user already gets a light artifact inside dark chrome. the only way to get a mismatch after this is to pin the chrome against your own os, which is an explicit choice.not trying to solve artifact theming here, that's #180.
known limitations
flagged by the review gate, left deliberately unfixed. shout if you disagree with any of these and i'll do them:
src/artifact-sdk.js), so a light chrome pops a dark card. that's the artifact side, so feat(design): make the default artifact theme configurable via LAVISH_AXI_DEFAULT_THEME #180.color-schememoved to:root, and the artifact iframe inherits the computed value. artifact markup and css are genuinely untouched, but ua styled surfaces in there (scrollbars, unstyled form controls) do follow the chrome preference. one path i didn't expect, documented rather than papered over.matchMedialistener, which is more js than this is worth.lavish-designskill under.agents/still describes the chrome as dark only, so prototyping from it produces dark only markup. it's machine generated, so regenerating it against the new palette is a separate job.one prerequisite bug
readStaterebuilt state fromsessionsalone, so every other top level key was dropped on the next session write.config.themewould have reset itself the moment you opened anything. fixed in the first commit with a regression test. stands on its own regardless of what you do with the rest.also in here
.framestays#fffon purpose, it's the artifact viewport, not chrome.--accentgoes dark brass because--accent-hoveris used as link text on paper, not just as a button fill.verified
matrix in a real browser via devtools color scheme emulation:
systemlightdarksystemfollows the os with no reload. dark resolves to exactly the values it had before, so nothing changes for anyone who stays dark.systemon a light ossystemon a dark oslight chrome, menu open
the mismatch case: chrome pinned
darkon a light osthe artifact follows the os, the chrome follows your pin. documented above.
Pipeline
Updates from git push no-mistakes
⏭️ **intent** - skipped
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
src/cli.js:256- writeThemePreference confirms a server is running, then on any POST /api/config failure silently falls through to writing state.json itself - the exact thing its own comment (and AGENTS.md) says is only safe when nothing is running. Concrete path: the user upgrades lavish-axi while an older server is still running (the version handshake only forces a restart from ensureServer on open/poll, not from config). runningServerBaseUrl accepts it because it only checkshealth.app === "lavish-axi", the POST 404s on the old server, postJson throws, and the CLI writes config.theme directly. The old server's readState still rebuilds{ sessions: parsed.sessions }, so it dropsconfigon its next session/prompt write, and it renders no data-theme-pref attribute anyway - yet the command reportstheme: lightas success. The same catch also writes concurrently with a live current-version server on any transient 500, where the atomic whole-file rename makes it last-writer-wins and can drop a just-queued prompt. fetchHealth already returnsversionand shouldRestartServer already exists: gate the fallback on a version match and either restart the stale server or return an actionable error instead of reporting a write that does not stick.src/chrome-client.js:725- whiteboardTheme() is evaluated only when the chrome posts lavish-whiteboard:init, and neither chrome-client.js nor whiteboard-frame.js listens for prefers-color-scheme changes. Under thesystempreference, changing the OS appearance while a whiteboard is open flips the chrome CSS live (the media query) but leaves the open Excalidraw frame and its shell on the previous theme until it is closed and reopened - a light chrome framing a dark canvas, which is the mismatch the new pinned-preference branch was added to prevent. README's Appearance section statessystem"follows the OS and switches live with it" and lists the whiteboard editor among the themed surfaces, so either re-post the theme on a matchMedia change or scope the live-switch claim to the chrome surfaces.🔧 Fix: fail config theme write beside stale server
1 info still open:
src/cli.js:1164- The README Appearance section was narrowed this round to say an already-open whiteboard keeps the appearance it opened with, but theconfigcommand help still carries the pre-narrowing sentence verbatim: "systemfollows the OS appearance and switches live with it", in the same paragraph that lists "the top bar, conversation panel, chrome overlays, and whiteboard editor" as the themed surfaces. Since whiteboardTheme() is read only when the chrome posts lavish-whiteboard:init (src/chrome-client.js:725), an OS appearance change undersystemstill leaves an open whiteboard on its previous theme - so this help string overclaims exactly where the README no longer does. AGENTS.md makes runtime guidance strings the owner of what agents are told while using Lavish, so the two documentation surfaces now disagree. Mirroring the README's wording here (chrome switches live; an open whiteboard adopts it on next open) closes it in one sentence, with no client JavaScript.🔧 Fix: align config help with narrowed theme docs
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
node --test test/session-store.test.js test/cli-output.test.js test/chrome-client-queue.test.js(166 tests, all pass)node --test test/server.test.js(164 tests, all pass; includes the newdata-theme-pref,/api/config, palette-parity and no-dark-literal guards)Manual CLI transcript against a live server on port 4491:lavish-axi config,config theme light|dark,config theme, plus rejection ofconfig theme sepiaandconfig colour; verifiedstate.jsonkeys are["sessions","config"]with 1 session preserved after the config writeManual browser verification:lavish-axi <artifact> --no-openthen headless Chrome Canary over CDP screenshotting/session/:keyat 1280x820 for theme=dark, theme=light, theme=system withEmulation.setEmulatedMedia prefers-color-scheme=lightand=darkManual browser verification of deeper chrome surfaces: overflow menu (hover tint + danger item) and the in-artifact annotation card captured in both light and darkManual browser verification of thesystemlive-follow claim: flipped emulatedprefers-color-schemeon an already-loaded page and confirmed the top bar switched while navigation count stayed 1 andperformance.timeOriginwas unchangedManual browser verification of the whiteboard editor on a Mermaid artifact: pinned light renders a light Excalidraw canvas, pinned dark renders a dark legible canvas even though the emulated OS reports light✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.