Skip to content

feat(chrome): light and system appearance for the editor chrome - #203

Open
pvinis wants to merge 12 commits into
kunchenguid:mainfrom
pvinis:pvinis/chrome-light-theme
Open

feat(chrome): light and system appearance for the editor chrome#203
pvinis wants to merge 12 commits into
kunchenguid:mainfrom
pvinis:pvinis/chrome-light-theme

Conversation

@pvinis

@pvinis pvinis commented Jul 28, 2026

Copy link
Copy Markdown

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.

#203, this one #204
mechanism :root[data-theme-pref="light"] plus a prefers-color-scheme media query every token carries both palettes via light-dark()
chrome.css vs main +102 / -16 +67 / -32
palette written twice, once pinned and once for system once
to keep in sync two blocks (there's a test pinning them equal) nothing
browser floor universal light-dark(), baseline may 2024
if unsupported n/a tokens invalid, chrome renders unstyled

short 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 browserslist or @supports anywhere in the repo, engines only 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 to system.

following your notes on #131

  1. device wide preference under a config command in state.json, not per browser localStorage. done.
  2. that preference drives the chrome. done.
  3. pass the theme to the agent as generation guidance instead of injecting a stylesheet. skipped, because it's moot here. point 3 was the alternative to injection, and this never touches the artifact at all: no injected css, no boot script, no opt out attribute. artifacts stay byte identical outside lavish. feat(design): make the default artifact theme configurable via LAVISH_AXI_DEFAULT_THEME #180 owns the artifact side.

also dropped from #131: src/appearance.js. it shelled out to osascript, the windows registry and gsettings to resolve system at cli time, and that only existed to serve the agent guidance half. the chrome runs in a browser, so prefers-color-scheme does it for free.

the chrome theme still resolves in pure css. no injected stylesheet, no boot script, no matchMedia listener.

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

system means 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 to dark and stay opt in. one line.

how

chrome.css is a render blocking <link> in <head>, so there's no fouc to defend against and no inline boot script needed. createChromeHtml emits <html data-theme-pref="..."> and the css resolves it:

:root[data-theme-pref="light"] { /* semantic tokens -> light ramp */ }
@media (prefers-color-scheme: light) {
  :root[data-theme-pref="system"] { /* same */ }
}

system going through a media query means it follows the os live, no matchMedia listener.

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-mistakes per CONTRIBUTING.md. it found two genuine bugs in the shared commits that i'd missed, both worth having:

  • state.json had a second writer. configCommand did a read modify write straight from the cli process, which breaks the invariant AGENTS.md leans on ("writes are serialized through the single server process"). concurrent with a server side queuePrompts, last writer wins and the other change is silently gone. fixed at the shared boundary: writeState is now a temp file plus rename, and config theme posts to a new POST /api/config when a server is live. it writes directly only when nothing answers /health, and refuses outright rather than writing beside a stale server.
  • the whiteboard ignored the preference. whiteboardTheme() read only prefers-color-scheme, so config theme light on 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.js never modelled documentElement, so the whiteboard change failed 15 tests there until it did.

pnpm run check is 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:

  • artifact with its own prefers-color-scheme dark mode: follows the os
  • artifact with no dark mode: stays light, always, in either chrome

with the default system that 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:

  • the annotation card and text highlight the sdk injects inside the artifact iframe are hardcoded dark (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-scheme moved 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.
  • a whiteboard that is already open keeps the appearance it opened with and picks up a change on next open. fixing it live means a matchMedia listener, which is more js than this is worth.
  • the lavish-design skill 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

readState rebuilt state from sessions alone, so every other top level key was dropped on the next session write. config.theme would 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

  • promoted the last dark only literals to tokens (hovers, the three scrims, the layout banner, danger tint, shadow color). no visual change, pinned by tests.
  • .frame stays #fff on purpose, it's the artifact viewport, not chrome.
  • light foregrounds tuned so every pair clears wcag aa on its own surface. --accent goes dark brass because --accent-hover is used as link text on paper, not just as a button fill.

verified

matrix in a real browser via devtools color scheme emulation:

pref os light os dark
system light dark
light light light
dark dark dark

system follows the os with no reload. dark resolves to exactly the values it had before, so nothing changes for anyone who stays dark.

system on a light os

system on a dark os

light chrome, menu open

the mismatch case: chrome pinned dark on a light os

the 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 checks health.app === &#34;lavish-axi&#34;, 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 drops config on its next session/prompt write, and it renders no data-theme-pref attribute anyway - yet the command reports theme: light as 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 returns version and 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 the system preference, 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 states system "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 the config command help still carries the pre-narrowing sentence verbatim: "system follows 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 under system still 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 new data-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 of config theme sepia and config colour; verified state.json keys are [&#34;sessions&#34;,&#34;config&#34;] with 1 session preserved after the config write
  • Manual browser verification: lavish-axi &lt;artifact&gt; --no-open then headless Chrome Canary over CDP screenshotting /session/:key at 1280x820 for theme=dark, theme=light, theme=system with Emulation.setEmulatedMedia prefers-color-scheme=light and =dark
  • Manual browser verification of deeper chrome surfaces: overflow menu (hover tint + danger item) and the in-artifact annotation card captured in both light and dark
  • Manual browser verification of the system live-follow claim: flipped emulated prefers-color-scheme on an already-loaded page and confirmed the top bar switched while navigation count stayed 1 and performance.timeOrigin was unchanged
  • Manual 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.

pvinis added 5 commits July 28, 2026 18:23
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.
@pvinis pvinis changed the title feat(chrome): light and system appearance for the editor chrome feat(chrome): device-wide theme preference for the editor chrome Jul 29, 2026
pvinis added 4 commits July 29, 2026 12:29
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.
@pvinis pvinis changed the title feat(chrome): device-wide theme preference for the editor chrome feat(chrome): device-wide light and system theme preference for the editor Jul 29, 2026
@pvinis pvinis changed the title feat(chrome): device-wide light and system theme preference for the editor feat(chrome): light and system appearance for the editor chrome Jul 29, 2026
@kunchenguid

kunchenguid commented Jul 31, 2026

Copy link
Copy Markdown
Owner

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 8f7806b1.

@kunchenguid

Copy link
Copy Markdown
Owner

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.

@kunchenguid

kunchenguid commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Holding this. lavish-axi config theme defaulting to system is a visible default-behavior change for light-mode machines — you already called that out — and #204 is still open as the alternative. I am not auto-merging default chrome appearance, and I am not rebasing a PR that needs that call.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(chrome): light and system appearance for the editor chrome

2 participants