Skip to content

feat(appearance): Liquid Glass theme - #908

Open
iceclatterWT wants to merge 78 commits into
eneskirca:mainfrom
iceclatterWT:feat/liquid-glass
Open

iceclatterWT wants to merge 78 commits into
eneskirca:mainfrom
iceclatterWT:feat/liquid-glass

Conversation

@iceclatterWT

@iceclatterWT iceclatterWT commented Sep 23, 2026

Copy link
Copy Markdown

Liquid Glass

Liquid Glass

New

  • Liquid Glass theme. Pick it in Settings → Appearance, next to Follow terminal / Dark / Light. Windows, menus, the sidebar, the dock, Settings and the kanban board all turn into frosted glass.
  • Desktop wallpaper. Put a picture behind the canvas. On a Mac you can use the built-in macOS wallpapers (read from your system, not bundled), pick a gradient, or choose your own image.
  • Glass slider. Go from Clear to Tinted. The "Readable" mark shows where text is guaranteed easy to read over any wallpaper.
  • Show grid dots on or off.
  • Keep blur while moving on or off. Off saves GPU while you pan and zoom.

Improved

  • Colours are cleaned up. In Liquid Glass every colour means one thing: blue for selection and activity, orange for "needs you", green for done, red for errors, yellow for warnings.
  • In Liquid Glass, sessions waiting for you glow with a soft orange light inside the window.
  • Claude Code, Codex and Grok look right on glass. Their panels become lighter glass instead of black blocks.
  • The minimap uses the same status colours as the windows.
  • Respects the macOS settings Reduce Transparency, Increase Contrast and Reduce Motion.

Good to know

  • Everything new is off by default. The normal look only gets small colour clean-ups (duplicate shades merged, "needs you" dots always red, a few light-theme text colours made easier to read).
  • Tested on macOS. Linux, Windows and the Server Edition get the gradients and the glass, but I haven't looked at them on screen.
  • Tests and typecheck pass. The only failing tests also fail on main.

https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ

@eneskirca

Copy link
Copy Markdown
Owner

I really like what you did! Thanks for your contribution!

Comment thread src/core/wallpaper.ts Fixed
@iceclatterWT

Copy link
Copy Markdown
Author

I really like what you did! Thanks for your contribution!

thanks, i'm working on converting the rest of the ui into glass theme also. curious to hear your thoughts once i have a working prototype

… guarantee

desktopWallpaper (none | preset | image, default none) and glassTerminals
(default false) are opt-in, so existing installs see no change.

glassTintAlpha picks the smallest tint alpha that keeps the theme foreground
at 4.5:1 over both a white and a black backdrop. The 0.95 design ceiling yields
to the guarantee for the two Solarized themes (Solarized Light is under 4.5:1
opaque, so it gets alpha 1).
…ache

scanStills reads /System/Library/Desktop Pictures at runtime (never bundled),
sips converts HEIC to JPEG into <userData>/wallpapers/, and load returns a
data: URL, so the renderer CSP is unchanged. The renderer never names a path to
read: stills resolve by id against a fresh scan, and an imported image must be a
hash-named direct child of the cache dir.

Registered in both shells; the Server Edition gets an empty stills list, and a
relay tab keeps a stub.
With glassTerminals on, a canvas terminal node gets a translucent tint of its
own effective theme (project override included) at the alpha glassTintAlpha
computes, plus backdrop blur, a translucent border and a specular header edge.
xterm paints no background: the theme background keeps its RGB with alpha 0 and
allowTransparency is set, so the WebGL atlas is rasterized without a baked-in
background. Both toggle live through applyLiveOptions (the WebGL renderer
rebuilds its atlas on any option change). The kanban card modal and the settings
preview never pass glass and are unchanged.
The React Flow root is viewport-sized and never transformed, so a background
set there stays fixed while the canvas pans and zooms. Gradient presets are
CSS; stills and imported images come from wallpaper.load as a data: URL and a
failed load falls back to the plain canvas. The dot grid is faded to 30% over a
wallpaper rather than removed, since snapping still aligns to it. The kanban
overlay paints its own background and is unaffected.
A thumbnail grid (None, the macOS stills core found, six gradients, Choose
image…) and a Glass terminals switch. Choose image… uses the existing file
dialog and core refuses anything but JPEG/PNG/WebP/HEIC; it is hidden in the
Server Edition, where the picker would browse the server's disk. Both keys join
Reset appearance.
The opt-in shared renderer paints text on a canvas below the nodes, so a glass
tint on the node would cover every glyph.
…render

Chromium drops a var()-containing value once it passes about 2 MB. A macOS
still's data: URL is about 3 MB, so the background shorthand that appended
var(--canvas-bg) resolved to nothing and the canvas stayed black. Set
background-image and its size/position longhands instead; the class rule
still supplies the canvas colour underneath.
The glass header is tinted with the terminal theme, but its title used the app
theme's --text, so a light app theme over a dark terminal theme read at 1.31:1.
Under glass the header redefines --text/--muted from the theme foreground.
Canvas re-renders on every drag frame; a fresh style object each time made
React re-diff the root's background-image.
Every glass node's backdrop moves under it on each camera frame, so the 28px
blur was re-rasterised for all of them. onMoveStart/onMoveEnd toggle a class on
the flow wrapper that drops backdrop-filter; the tint, which is what the
contrast guarantee rests on, stays.
A null result pinned the plain canvas for the app run even after the file
became readable.
…he files

Imports were copied at full size, so a large photo passed import and was then
silently refused by load's 25 MB cap. On macOS every import now goes through
sips to a JPEG no larger than 3840px (sips -Z also upscales, so it is passed
only when the image is bigger). Elsewhere an image over the cap is refused at
import time, where the picker shows the reason.

A saved change of desktopWallpaper, and each import, delete hash-named cache
files nothing refers to. Thumbnails, temps and foreign files are never touched,
and there is no prune at boot: an unreadable settings.json loads as defaults and
must not cost the user their chosen image.
…and black

Endpoints suffice only while the text's luminance lies outside the range of
composite luminances; inside it some backdrop matches the text exactly. That
case now fails explicitly, and a grey-backdrop sweep pins the claim per theme.
Foreground alphas are unchanged for every built-in theme.
The tint guarantees 4.5:1 for the theme foreground only. Holding the ANSI
palette too would make every built-in theme opaque, so the glass keeps the
foreground-only guarantee and the Settings copy now says so.
Re-encoding every macOS import to JPEG flattened PNG/WebP alpha and recompressed
JPEGs for nothing. sips now runs only for HEIC, an image over 3840px, or one
over the 25 MB load cap.
onMoveStart/onMoveEnd are useCallback'd, and the blur comes back 150 ms after
the last move ends so a setViewport-driven wheel zoom does not toggle it per
packet. Comments now say what the failed-load cache and the header tokens
actually guarantee.
… chrome tint

The Glass terminals switch is replaced by a fourth Appearance option, Liquid
Glass, whose light/dark base follows the terminal theme like Follow terminal.
Choosing it with no wallpaper picks one (the first macOS still, Sonoma Horizon
when present, else a gradient) so glass never sits over plain black. The
pre-release glassTerminals key maps to it once.

glassChromeAlpha computes the chrome fill's alpha for the app theme's --text
over --panel. --text is itself translucent, so the foreground moves with the
backdrop and the white/black endpoint argument no longer holds; the backdrop
is sampled instead (6x6x6 grid plus a grey ramp) and the tests re-check a
fine grey sweep for both themes against the real tokens: dark 0.70, light
0.745. App.tsx publishes it as --glass-chrome-bg with data-nt-glass on <html>.
…Liquid Glass

All of it is gated on data-nt-glass, so the other appearances are unchanged.
Surface tokens (--panel, --surface-*, --tabbar-bg) become the computed chrome
fill, which covers the panels that paint them; the floating containers that
hard-code rgba(var(--menu-rgb), ...) are listed. Blur goes on outermost
containers only (a nested blur re-blurs its parent's pixels); node blur pauses
while the camera moves, chrome keeps it.

Terminal windows drop their per-node accent: neutral top edge and colour dot
(both inline styles, hence !important), neutral resize handles. Selection is a
neutral ink outline; unread keeps its accent border and glow. The sessions list
and kanban keep node colours. A stylesheet test pins the gate and the
neutralising overrides.
The wallpaper text still pointed at the removed Glass terminals switch, and
the Appearance description had grown to a paragraph. It is two sentences now,
with the contrast note shown only while Liquid Glass is selected.
…n Liquid Glass

The wallpaper is painted on .canvas-root, which spans the window including the
tab bar row, so the Liquid Glass tab bar is glass over the picture; React
Flow's root goes transparent over it and the picture still does not move with
pan/zoom. The tab bar's fill is the computed chrome tint, whose 4.5:1 holds
over any backdrop, bright sky included.

Minimap node rectangles were painted in each node's colour, the accent Liquid
Glass removes. They are translucent theme ink now (brighter when selected);
working/attention/unread strokes keep their colours, and the mask uses the
chrome fill.
settings.canvasDots (default on in every appearance) decides whether the canvas
renders its React Flow dot Background. Display only: snap-to-grid and
align-to-grid keep using gridSize. Hand-edited values other than a literal false
keep the dots. Pure renderer, so the Server Edition gets it too.
Status and accent colours become tokens instead of hues typed at each call
site: the Apple system palette (--sys-red ... --sys-gray, dark and light
values) and semantic roles that rules name (--state-working, -attention,
-unread, -error, -success, -warning, -queued, -automation and the
--git-modified/added/deleted/renamed/conflict letters). About 120 CSS sites
and the TS literals now read a role, so an appearance can re-map a meaning in
one place. lib/palette.ts holds the same table for the JS that needs a
literal (xterm find decorations, canvas sprites, the notch HUD), pinned to the
CSS by styles.palette.test.ts.

Git file-status colours come from one table (lib/gitStatusColors.ts) shared by
Source Control and the history commit list. The kanban priority colours are
defined once (CardMetaBar) and read by SessionCard.

Fix: the minimap status strokes were crossed against the node glows (working
drew yellow, unread drew clay). They now read the same roles as the glows.

The default look keeps its historical hues for every feature. It changes only
where duplicates collapse:
- Minimap strokes and halos: working yellow -> clay, unread clay -> accent
  blue (the fix above).
- Greens: #32d74b and #30d158 collapse to system green #30d158 (--success,
  git "A", diff-add text, git-graph lane 1, trigger/phone/account success).
- Loop/schedule purple #bf7af0 -> system purple #bf5af2 (loop status, loop
  card, trigger accents, loop resize handle).
- Reds: GitHub PR-closed #f85149, the SSH error dot #ff6b6b and the git
  history error text -> the danger token; the SSH connection banner's
  ad-hoc dark red/amber backgrounds are now mixed from the error/warning role
  over the overlay surface (near-identical in dark, a readable pale tint
  instead of dark red under dark ink in light).
- Queued-warn chip #ffb340 -> the warning token.
- Queued/disarmed/muted grey #8e8e93 -> system gray #98989d (dark).
- Diff line washes: the Tokyo-Night green/pink (158,206,106 / 247,118,142)
  -> system green/red at the same alphas.
- Diff hunk headers: teal #6ac4dc -> accent text (the info role).
- Git graph lane 3 teal #6ac4dc -> system teal #40cbe0; conflict status
  letter the same.
- Terminal find highlight #ffd54f/#ffb300 -> system yellow/orange.
- Accent washes (about 20 hard-coded rgba(10,132,255) sites incl. the unread
  glow) follow the Accent setting; with the default accent nothing changes.
- Light theme: fixed-hex status fills take Apple's light system values; git
  status letters, kanban priorities and the warning/success text in Settings
  take the theme's text-safe tokens instead of dark-theme hex on cream; the
  unknown git status letter draws in --text instead of white.
- Mascot clay rgb(217,120,87) -> the Claude brand colour #d97757.

Not changed (each would move a whole feature's hue in the default look):
working stays clay, unread stays accent, attention stays red, warnings stay
orange, git renamed purple / conflict teal, the rope default colour, the
onboarding dots, node-kind default colours (persisted into project files),
the node colour swatches (an allowlist for stored colours).
Under Liquid Glass each status meaning takes its Apple colour and no hue is
shared between two meanings: working and selection use the accent (not the
Claude clay any more), needs-you is orange, finished-but-unseen is green,
warnings are yellow and errors red. Only the role tokens are redefined, so
the node glows, minimap strokes, sidebar signals, header badges and git
letters (renamed = accent, conflict = red) follow without per-rule overrides.

Status labels on a glass header become ink over a chip tinted with the status
hue, as HIG asks for colour on glass: the hue lives in the background and the
dot, the text stays monochrome.

The remaining hard-coded unread and needs-you text colours now read the
--state-unread role and a new --attention-text token; both resolve to their
previous values outside Liquid Glass, so the default look is unchanged.
… spacing

Visual QA M7 (label row stacked a third band on glass), M6 (swatch abutted the theme name), L2
(pill separator hugged the next token).
The line-endings guard added upstream (issue eneskirca#578) flags reads that slice
on a newline literal without normalizing CRLF checkouts first.
An element with its own backdrop-filter is a backdrop root: anything inside it blurs only the root's pixels. The dock's Zoom/Layouts/Add menus (and the Add submenu) therefore showed sharp terminal text straight through, and the context-window and colour popovers inside glass nodes had a translucent fill and no blur at all.

- .dock, .dock-menu and .dock-menu__sub keep their glass on a ::before layer (fill, hairline, blur), so none of them is a backdrop root and every menu blurs the canvas. The dock keeps the control fill; its menus are text surfaces.
- .ctx-popover and .color-popover (inside a glass node, which is their backdrop root) are solid rgb(var(--menu-rgb)) under glass.
- styles.palette.test.ts: the ctx-popover test pins the solid fill; the status-hue literal guard scans declarations only (a selector may name a hue it repaints).
…:1 at Clear (visual QA N2, N5, N6, N12, H4)

- glassChromeAlpha takes the highlight washes that stack on the fill (ink lift, accent selection) with the ink they carry, and solves the readable alpha for all of them. Highlighted rows, the active tab and the selected segment use --text-strong. Dark chrome 0.70 -> 0.74, light unchanged 0.745. Active tab label at Readable 3.95 -> 5.09:1, its icons 2.0 -> 3.9:1 (opacity 0.8 on the active tab); light palette active row 4.28 -> 5.91:1.
- glassControlClearAlpha: small controls at Clear keep 3:1 icons over every sampled backdrop after the control blur's brightness(--glass-control-dim). Dark stays 0.35; light rises to 0.57 (brightening cannot lift near-black water).
- Hovers that re-stacked --panel-header (dock buttons, zoom pill, sessions rows, default Button) are neutral lifts (N5).
- Segmented control selected state is a neutral lifted thumb, so the one filled accent per view is the primary action (N12).
- ::placeholder uses --glass-placeholder (0.78 dark / 0.86 light), pinned at 4.5:1 on the fill by the test (N6).
…and label polish (visual QA N3, N4, N8-N11, M1, M3, M6, L4)

- N3: '+ Add column' joins the text-surface glass list with --text ink (1.89 -> 7.25:1 dark over the photo, 1.19 -> 5.96:1 over Sunrise, 1.96 -> 6.36:1 light).
- N4: on the board the toolbar cluster drops into the header strip's row.
- N8: the usage pill drops whole trailing segments instead of eliding mid-token.
- N9: saturation 1.5 from the Readable tick to Tinted, 1.8 only at Clear (text surfaces 1.5).
- N10/L4: refresh button 26px like the RAM pill; the zoom column is 34px like the sessions toggle.
- N11: the project menu opens below the glass tab bar (tab-menu--tabbar).
- M1: OK-range meters and session context chips are neutral ink under glass (yellow/red keep their meaning); the fills are inline literals shared with the HUD, so the rule matches the serialised green and the test pins it.
- M3: card modal comments title, hint, times and events take --muted.
- M6: the terminal theme trigger left-aligns its name; .bind-select is an input well on glass.
Everything is behind :root[data-nt-glass='on']; the default look is unchanged.
…d (visual QA N7, L5)

- The imported-wallpaper tile paints the image itself (useWallpaperBackgroundFor, the canvas's own loader and cache) instead of the words 'Your image', which stay as its label and tooltip.
- The chosen wallpaper tile carries a check badge, not only a ring.
- Accent swatches expose aria-pressed like the wallpaper tiles.
…r (visual QA round 3 NC1, NC2, NH1, NH2)

Under Liquid Glass the shared surface tokens (--panel, --panel-header, --panel-2, --surface-*,
--tabbar-bg) used to BE the translucent fill, so every floating surface nobody had listed became
see-through with no blur. They now keep their solid theme colours; only the listed surfaces get the
fill, each with a working blur.

- NC1: the Explorer and Source Control drawers join the text-surface glass list (blurred).
- NC2: context menus that host flyouts carry their glass on ::before like the dock, so they are no
  backdrop root and the Snap to zone flyout blurs the canvas. Scrolling menus and leaf flyouts keep
  it on the element (a ::before would scroll away; they host no flyout).
- NH1: the node "+ Label" picker and the card modal's Labels and Members pickers paint their opaque
  token colour; the explicit solid rule for the context/colour popovers is no longer needed.
- NH2: tooltips are text-surface glass (portalled, so the blur works); dock tooltips clear its rim.
- Pieces inside glass that painted a token take a lift, the input-well sink or nothing (find bar,
  markdown bar, session chips and fields, Settings sidebar, hover rows in drawers/sidebar/cards).
- loop and subagent nodes take the chrome fill explicitly (they had it through the tokens).
- Guards: styles.glass-traps.test.ts fails when a rule paints a glass fill on a selector without a
  backdrop-filter or a paired ::before blur (fails on the previous stylesheet: the :root token
  block and the drop-target rule); scripts/glass-trap-probe.mjs walks the live page over CDP and
  reports translucent surfaces whose blur is ineffective (0 across 27 overlay states).

Everything is behind :root[data-nt-glass='on']; the default look is unchanged.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
Code review 5:
- eneskirca#1 the hover lift (dock buttons, session rows, the default Button) carries --text-strong: --text
  on it was 4.11:1 dark / 4.04 light, and the lift is not in the solver's highlight list.
- eneskirca#2 a Layouts row is one highlight: its main button no longer stacks a second lift (3.48:1), and
  takes --text-strong while the row is hovered or focused (3.78:1).
- eneskirca#3 the wallpaper cache keeps the two most recent images: opening Settings ("Your image" tile)
  evicted the canvas still, so the board's next first frame went black and re-read 3 MB.
- eneskirca#5 unselected segments get their hover colour back under glass.
- eneskirca#6 the project menu's glass offset moves into openMenu (the tab bar's bottom edge) instead of a
  `top: … !important` that defeated useMenuFlip; the tab-menu--tabbar class goes.
- eneskirca#7 the usage pill's first segment always keeps line 1 and elides there; the error mark leads.
- eneskirca#8 the highlight-aware readable alpha is tested for every accent swatch, bound 0.85 (Yellow
  0.825, Teal 0.76 dark).
Visual QA round 3:
- NM1 typed text in chrome fields is --text-strong, so an empty field reads empty; the light
  placeholder is 0.85 (its 4.5:1 floor, never above --text; pinned by the test).
- NM2/NM3 kanban column count, close, "+ New session", card-modal meta labels, the column badge
  and priority chips take --muted with lift hovers; the activity author is ink, weight 600.
- NL1 the three dock menus clear the dock by 8px; NL2 the RAM panel rises above the dock and long
  names elide; NL3 the theme list lines its swatch/name columns up with the trigger, chevron
  centred; NL5 the current Speech language row is the selection wash; NL7 active-tab icons 0.9;
  NL8 a menu without icons keeps no icon column (Help).
- NM4/M2 (selection ring at 45%) and NL4/L1 (tab-menu chevron) stay skipped, NL6 (dark glass over
  bright pastels) is left as physically correct dark glass.

Everything is behind :root[data-nt-glass='on']; the default look is unchanged.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
Corrects the false "those five were the only traps" claim (round 3 found six more), documents the
opt-in token model, the ::before host rule for context menus, the static guard
(styles.glass-traps.test.ts) and the live probe (scripts/glass-trap-probe.mjs, 0 traps over 27
overlay states), the hover-lift ink, every-accent readable alpha and the NM1 placeholder/typed ink.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
…tion (visual QA round 4 N4-H1, code review 6 eneskirca#5-eneskirca#7)

An element with opacity < 1 is a backdrop root. The scrims faded their opacity, so the palette,
dialogs, drawers and card modal inside them blurred only the dim scrim and the canvas read sharp
through the tint for 120-160 ms, then snapped to frosted; the ::before-hosted menus faded their
own opacity the same way. Under Liquid Glass scrims now fade their background colour, glass
surfaces enter by transform only, tooltips appear without motion, the focus-mode dock slides and
its exit pill rests opaque. The default look keeps its animations.

Static guard (styles.glass-traps.test.ts): fails on opacity keyframes or transitions on any glass
surface or scrim unless a gated rule overrides them; background-image counts as a paint; the last
backdrop-filter declaration decides; coverage keeps :not()/:hover and compares the full selector.

Live probe: a mid-animation pass (replays every finite animation, freezes all at 50%, scans,
restores); a blur inside a see-through backdrop root and a blurred surface fading its own opacity
are traps; ::after layers, gradient fills and mask-border roots are inspected; the scrim list is
shared with the static test; the entry guard resolves symlinks and a probe that checked nothing
exits 2 instead of 0.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
…bels and QA round 4 lows (N4-M1-M3, N4-L1-L8, code review 6 eneskirca#2-eneskirca#4, eneskirca#8)

- N4-M1: the four popovers that cannot blur (context meter, colour, label and members pickers)
  paint one solid token under glass, --panel (the colour the glass fill is --panel at an alpha
  of), with the glass hairline and one shadow; Remote access, Publish, notification consent and
  the GitHub issue dialogs join the glass text surfaces, so every modal dialog is glass.
- N4-M2: the glass placeholder no longer reaches the opaque popovers; they keep the theme's.
- N4-M3: the sessions sidebar project name is --text-strong, the unpinned pin is ink.
- Lows: one glass hairline on every menu and sheet (L1); Members hover is the selection wash (L2);
  ink checkmark on the washed speech row (L3); RAM rows reserve the pause slot (L4); the terminal
  theme list scrolls inside the glass menu and fades its end (L5, a display:contents wrapper in
  the default look); the node label picker clears the node's rim (L6; its repeated copy is
  upstream and unchanged); dock icons draw opaque ink faded as one layer (L7); palette rows take
  the strong ink so the placeholder reads lighter (L8).
- Code review 6: the default Button keeps --text-strong on its own hover (eneskirca#2); sessions row
  actions drop the opaque --panel-header fade and the title line makes room (eneskirca#3); the dino
  header is a lift, not an opaque band (eneskirca#4); the wallpaper cache evicts least recently used (eneskirca#8).
  eneskirca#9 (pause the pinned drawer's blur while panning) is skipped: .canvas-moving lives on the flow
  wrapper, which the drawer is not inside, and the review marked it optional.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
…probe leaves no state; QA round 5 fixes (code review 7, N5-M1, N5-M2, N5-L1-L3)

Code review 7:
- eneskirca#1 (HIGH): under Reduce Motion + glass the kanban card modal and its scrim (also the GitHub
  issue modal's) still ran their opacity fades, kanban-fade/kanban-pop: the glass overrides sat
  only in a no-preference query. A gated reduce rule switches them off. Live, reduced motion with
  playback frozen: 0 animations, opacity 1 up the chain; reverted, the scrim ran kanban-fade at
  opacity 0.
- eneskirca#2: the static guard checks both motion preferences separately; an override inside a media
  query counts only for the preference it names. Reverting eneskirca#1 now fails with exactly
  .kanban-modal-scrim and .kanban-modal.
- eneskirca#3: a fade matches a glass surface when their selectors can hit the same element (either covers
  the other, or they share a subject class).
- eneskirca#4: a blurred ::before layer is a target, so its own opacity fade is caught.
- eneskirca#5: a transition shorthand with no property name counts as all; @-webkit-keyframes count; a
  later unconditional rule setting backdrop-filter without a blur on the same element cancels the
  blur. Each evasion has a synthetic case; all three new cases fail on the previous guard.
- eneskirca#6: the probe's mid-animation pass no longer calls pause()/play(). It seeks only finite running
  animations and seeks them back (one synchronous evaluate, so the timeline cannot advance),
  re-sets animation-name on the elements it replayed, removes a style attribute it added, and
  checks its own restoration (exit 2 if anything is left behind).
- eneskirca#7: the probe times out: 20 s per evaluate, 5 s for the target list, then exit 2.
- eneskirca#8: the mobile-launch card is glass like every other modal dialog.

Visual QA round 5:
- N5-M1: destructive menu items on glass have an ink label, and only the icon stays red. Delete
  measured 7.37:1 dark / 6.16:1 light (was 2.79 / 3.89); the red icon is 3.38 / 3.30.
- N5-M2: the placeholder cannot go below 0.77 on dark glass and keep 4.5:1 over the sampled
  backdrops, so the dark glass dialogs raise their secondary text (--muted) to 0.82, and Remote
  access's hard-coded 0.55 lines use it. Placeholder ink is now below secondary ink. In light,
  0.85 is both the placeholder floor and --text, so typed text stays --text-strong.
- N5-L1: Remote access pops like its sibling dialogs.
- N5-L2: RAM panel secondary text uses --muted: 6.6-7.5:1 dark, 4.95-5.8:1 light (was 3.95-4.22).
- N5-L3: the Report a bug and Set icon titles are semibold, like Remote access.

The default look is unchanged: every new rule is behind the glass gate.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
…s, doc fix

- scripts/glass-trap-probe.mjs: move the mid-animation replay/seek loops inside
  the try block so the finally restoration always runs; restore an element that
  already had a style attribute via setAttribute('style', before) instead of
  poking el.style, and remove the attribute only where there was none; note in
  the header that replaying an animation fires animationcancel/animationend,
  which the app may react to (e.g. the drawer refits maximized nodes and marks
  the workspace dirty).
- src/renderer/styles.glass-traps.test.ts: TIMING regex now accepts an optional
  leading minus so a negative transition-delay isn't mistaken for a property
  name; added a synthetic case (`.pane { transition: 0.2s -0.05s }` on a glass
  surface) that fails under the old regex and passes under the fixed one.
- CLAUDE.md: drop the inaccurate claim that `.tab-menu button.danger` keeps red
  on its icon under glass — nothing renders or styles that; the sentence now
  describes only `.ctx-item.danger`.

Claude-Session: https://claude.ai/code/session_011MqqEsWfh1xKLv4fip3aPZ
@iceclatterWT iceclatterWT changed the title feat(appearance): desktop wallpaper and glass terminal nodes feat(appearance): Liquid Glass theme Sep 24, 2026
Enes Kirca and others added 2 commits September 24, 2026 12:19
…s glows

toDataUrl stat()ed the file and then readFile()d it by path, so the size
cap could be checked against one file and the bytes read from another
(CodeQL js/file-system-race). Open once, fstat and read the same handle,
and refuse a non-regular file.

Glass terminal nodes are 14px-rounded while the unread/working/attention
glows are drawn at 10px, so the ring sat off the corners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… files

The import-time prune kept only the wallpaper on screen, so with a preset
showing and a recent import A, importing B deleted A before the renderer
saved the choice; a reload in between left recentWallpaperImage naming a
missing file. It now keeps everything the saved settings name.

A FIFO or device named x.png hung copyFile/sips and wedged that target's
publishOnce job for the run; import now requires a regular file.

The docs said the renderer never names a path to read; import does take
one. Say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread scripts/glass-trap-probe.mjs Fixed
Comment thread scripts/glass-trap-probe.mjs Fixed
Comment thread scripts/glass-trap-probe.mjs Fixed
Comment thread scripts/glass-trap-probe.mjs Fixed
installGlassCellBackgrounds patches the WebGL addon's shared renderer
prototype, and it ran on every WebGL grant whether or not glass was on,
so a user who never enabled glass still rendered through a patched
private renderer. Install it when a glass terminal needs it: at grant
time if the node is already glass, else when its alpha turns non-null.

Also: a run whose buffer line is missing read the shared scratch cell
left by the previous run; treat it as unknown instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eneskirca

Copy link
Copy Markdown
Owner

Thanks for this, it's a big and careful piece of work. I reviewed the current head (ea537982), pushed four fix commits on top, and I'm merging.

A note on the rebase first: yesterday I pushed a fix commit to this branch (e252f07d), and the force-push dropped it. I've re-applied it below. If you rebase a branch someone else has pushed to, please fetch first so their commits come along.

What I pushed

  1. toDataUrl stat-then-read race (the CodeQL js/file-system-race alert). The size cap was checked with stat(path) and the bytes read with readFile(path), so a file swapped in between could bypass the 25 MB cap. It now opens once and runs fstat and readFile on the same handle, and refuses anything that isn't a regular file.
  2. Glass node glows. Glass terminal nodes are 14px-rounded, but the unread/working glows are drawn at 10px, so the ring sat off the corners.
  3. Import prune and the recent image. importWallpaper pruned against desktopWallpaper only. With a preset on screen and a recent import A, importing B deleted A before the renderer saved the choice. A reload in between left recentWallpaperImage pointing at a missing file. It now keeps everything wallpapersToKeep names.
    • It also refuses a non-regular source file. A FIFO named x.png hung copyFile/sips and left that target's publishOnce job pending for the rest of the run.
    • The docs said "the renderer never names a path to read", but import does take one. Both the module header and CLAUDE.md now say so.
  4. The cell-background wrap for users who never enable glass. installGlassCellBackgrounds patches the WebGL addon's shared renderer prototype, and it ran on every WebGL grant. So a user who never enabled glass still rendered through a patched private renderer. It is now installed only when a glass terminal needs it: at grant time if the node is already glass, or when its alpha turns non-null.
    • Also in this commit: a run whose buffer line was missing read the shared scratch cell from the previous run. It is now treated as unknown.

Checked on the head merged with main: npm run typecheck is clean, and the full vitest suite passes (13,109 passed, 0 failed).

Changes in the default look (accepted, recorded for the changelog)

The description says nothing changes unless you opt in. That holds for the wallpaper, glass and Liquid Glass code, which is well gated. The semantic-colour refactor and one Reduce Motion block do reach the default Dark and Light looks. We've decided to accept these as intended, so they're listed here for the record:

  • NEEDS YOU is red everywhere. The kanban badge and the session-memory dot were orange (--warn) while the node header was already red. They now match.
  • Reduce Motion now also stops the unread/working/attention glows, the status dots and the minimap beats, for every appearance and not only Liquid Glass.
  • Dark theme hue shifts, now the system colours:
    • --success #32d74b → #30d158
    • git lane 3 / conflict letter #6ac4dc → #40cbe0
    • diff hunk header cyan → accent blue
    • loop purple #bf7af0 → #bf5af2
    • queued grey #8e8e93 → #98989d
    • closed-PR red #f85149 → #ff453a
    • danger-hover pink → #ff453a
  • Light theme: state colours, git letters and lanes, priority chips, the Settings status texts and the RUNNING badge move to darker light-theme values. These are mostly legibility improvements.
  • Custom accent: about 20 washes that were hard-coded rgba(10,132,255,…) now follow var(--accent). They're identical at the default accent and change colour with a custom one.

Downgrade and open items

  • Downgrade: a build older than this one maps the unknown appTheme: 'liquid-glass' to dark. Nothing breaks.
  • Not fixed, noted for later:
    • Server Edition hides "Choose image…", but wallpaper:import is still registered there. That's no wider than the server's own fs:read.
    • On Server Edition with two tabs open, a tab holding stale settings can save over the other tab's wallpaper choice, and the prune then deletes the image that tab just picked.

Visual verification so far is your macOS dev build. We'll check it on our side too.

Every value the probe prints comes back from the page over CDP; a newline
or escape in one could forge log lines (CodeQL js/log-injection x4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
for (const r of results) {
traps += r.traps.length
if (!r.glass || r.checked === 0 || r.leftovers.length) blind = true
for (const l of r.leftovers) console.error(`probe left state behind: ${clean(l)}`)
for (const l of r.leftovers) console.error(`probe left state behind: ${clean(l)}`)
if (args.includes('--json')) continue
if (!r.glass) console.log('Liquid Glass is not on (data-nt-glass) — nothing glass to check')
console.log(`${clean(r.pass)}: ${clean(r.traps.length)} trap(s) in ${clean(r.checked)} translucent surface(s)${r.pass === 'mid-animation' ? `, ${clean(r.animations)} animation(s) seeked to 50%, restored` : ''}`)
if (args.includes('--json')) continue
if (!r.glass) console.log('Liquid Glass is not on (data-nt-glass) — nothing glass to check')
console.log(`${clean(r.pass)}: ${clean(r.traps.length)} trap(s) in ${clean(r.checked)} translucent surface(s)${r.pass === 'mid-animation' ? `, ${clean(r.animations)} animation(s) seeked to 50%, restored` : ''}`)
for (const t of r.traps) console.log(` ${clean(t.el)} ${clean(t.bg)} ${clean(t.why)} [${clean(t.box.join(','))}]`)
})()
if (isMain)
main().catch((e) => {
console.error(String(e?.message ?? e).replace(/[\r\n]/g, ' ').replace(/[\u0000-\u001f\u007f]/g, ''))
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.

3 participants