Every radius in the chrome and the panel scales with the theme's own radius (--wb-c-rs = radius / 12,
0 to 1.5): a square theme gets a square window, a round one a rounder window; themes that keep the
site's colors keep wayBetter's own corners. A theme built from raised and sunken edges (ChromeTheme
edges: 'bevel') gets frame/edges.css on html[data-wb-edges=bevel], also from the first frame: a face
color under tabs, toolbar, menus and the panel, 98.css's four edge lines derived from that face,
the active title bar's gradient across the tab row, tabs as a tab control, small raised caption
buttons, a sunken address field, selected menu rows in the title bar's color, raised and default
buttons, etched group boxes and classic scrollbars in the panel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShellMenu.trigger chooses the trigger: 'pane' (default; each pane's wB button opens the menu under
itself, lined up with its end edge and kept inside the window), 'button' (titlebar) or 'logo'.
Alt+F, F10, the palette and the account menu's last row open it under the focused pane's wB (the
first one on screen otherwise). The menu's Settings row and the other panel rows still open the
panel, and Ctrl+Shift+W still does. The logo is part of the drag region again, as before e5d43af.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A theme rule gated by html:has(...) / body:not(:has(...)) made the browser
restyle the whole page for every node the site inserted. Measured live on
x.com under any theme: about 440 elements restyled per insertion, against 1.5
without a theme; with the gates gone, 2.5.
Sites name page states as selectors (ThemeRuntimeSite.flags). The engine
keeps html[data-wb-<name>] in step, checked once per DOM hub frame and also
inside the site's open shadow roots. It passes the flags that hold to css(),
so CSS for one state can be left out of the other. Changing flags rebuild the
theme (they are part of its render key). Tests: pageFlags, flagAttribute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live on X's chat embed: --x-white is declared only inside the embed's root, so
the document's values left .text-white unresolved and the sent bubble's label
stayed white on a light accent (2:1 under Nord). Each root's theme now reads
the site's primitive variables from the root's own content (our sheet off
while reading), falling back to the document's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS the shell can hold the keyboard while its own settings tab is active; Ctrl+F then did
nothing, so typed keys and Enter went to X's search field. Find now targets the last web tab that
was in front (or the first one on screen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keys that open the find bar are often pressed in the page. The bar asks for the keyboard (`shell_focus`) and focuses its field; if the shell still does not have the keyboard 250 ms later, it asks once more and focuses the field again, so typing never lands in the page (seen on macOS: text typed right after Cmd+F was lost). Diagnostics lines record both steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X renders its chat inside the open shadow root of its embed host since
2026-10-01; the theme's <style> in <head> never reached it. For each root the
DOM hub names (domRoots), the engine now builds the theme from that root's own
rules (collectRootRules: adopted sheets and <style> elements, its own sheet
excluded), adds the static sheet, adopts it last among the root's sheets,
rebuilds it when the root's rules change and takes it off when the theme is
off or the root is gone. Test: collectRootRules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X renders its chat inside an open shadow root since 2026-10-01 (the XChatEmbed
host [data-testid=xchatEmbedRoute]); the hub's document observer saw nothing
of it, selector observers found nothing, feature sheets in <head> did not
reach it and probes could not see it.
A site adapter may name shadowHosts. The hub's one MutationObserver then also
observes each such root (picked up on the next page mutation, or a few timed
retries while a host has no root yet; dropped with the observer), selector
observers search the roots too, domRoots(win) gives features the roots, a
feature's ctx.css sheet is also adopted by each root (CSP-proof, updated in
place, taken off on stop), and probes count matches inside the roots.
Tests: hub observes and searches a root that appears later with one
observer; registry adopts and removes the sheet and a probe inside a root is
never a miss.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
themeDoctor(theme) names the lighter mechanism for what a theme's CSS does
(radius, bevel, hiding, literal colors, fonts), its performance costs (:has,
universal selectors, blur, size) and accessibility costs (outline removed,
unguarded motion, small post text, moved navigation), plus contrast fixes.
Theme import adds the count and the first hint to its message. Tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Appearance > Icons gets Import icon pack: a *.icons.json file is parsed by the
runtime sanitizer and shown on an install screen (name, author, licence,
source, credits, every glyph, size, data only) before it joins the persisted
theme choice (packs, at most 8, validated natively for shape and size) and
the Icons picker. Imported packs can be removed again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the logo as the menu's only titlebar trigger (`<AppMenu trigger={false}/>`), `--bt-tb-menu` still kept 38px free at the end of the titlebar-hosted tab strip. It is 0 when there is no `.bt-app-button`, so the right side is back to its layout before the menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The top-right ⋮ and the logo opened the same menu: one of them was redundant.
The logo stays as the only titlebar trigger (the app's own mark, where an app
menu lives); the right side is back to workspaces, accounts and the window
buttons. Alt+F, F10, the palette and the account menu's last row open the
menu under the logo. Apps that want a separate button keep <AppMenu/>'s
default trigger.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ThemeChoice gains an optional icons field (absent: follow the theme's pack; site: the site's
own icons; else a pack id), validated natively as a slug (waybetter-host theme.rs, with a
test) and kept by per-theme choices; variants is an override group natively too.
- Appearance: an Icons row (Follow the theme / the site's own icons / the site's packs) when the
site lists packs; English words in the catalog.
- The chrome's glyphs follow the owner's pick as well as the theme's suggestion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For the macOS reports (the menu never shows and the window goes inactive on a click; the find field gets no keyboard): off unless the app is started with `--diag` or WAYBETTER_DIAG=1. Then native code and the shell append to `diag.log` in the app's log folder (truncated at launch): menu open requests and every close with its reason, overlay counts, raise/lower and `shell_raise` results, native `shell_focus` calls and their result, the shell document's focus/blur/visibility, and where the find bar's focus is right after it opens and 300 ms later. A line is an event and a few words in plain ASCII (capped); never a URL, a query or page text. `diag_get` / `diag_log` are shell-only (in commands::SHELL and the template's list).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site's theme adapter may name the pack of the global theme (iconPack); the frame's Icon draws
that pack's glyph for the same meaning (home, search, bell, mail, bookmark, user, ...) from its
validated path data, and its own line glyph where the pack has none. Rendered as React SVG
elements, never markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seen live: the report said "WebView2 154.0.0.0", Chromium's reduced user agent version. The engine's brand list (`fullVersionList`) gives the full one, which the report now uses when it is a plain version (tested).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menuMessages.en.json stays the English source of the menu's words; en.json now carries them too
(the i18n test takes them into the source union) and 48 catalogs translate all 51 menu words and
the seven new mode words (plural forms per language for the match count).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release blocker (macOS 1.0.0: both panes stayed on the shell's loading shape). After `shell_ready` the startup cover waited, with no limit, for every visible site tab to report that it painted. A page whose report never comes (its paint check is frame-driven and gives up after 10 s, and an engine may hold a hidden window's frames back) kept the shell raised over the panes, showing the skeleton, for good. The wait is now bounded (8 s after the window is shown); the page then shows as it is.
The app menu asked for the keyboard natively (`shell_focus`) on every open. It now does so only when the shell does not have it (keys pressed in a tab); a click on the shell already gave it the keyboard.
The mode cards' default picture was shrunk to 16px by the shell's base glyph rule ("a few dots"); it keeps its size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebView2 runs document-start scripts before the document has an element, so
injectCSS threw ("reading 'appendChild'") for every CSS feature that was on at
start, e.g. hiding promoted posts, until the next flags delivery restarted it.
Such styles now wait (one MutationObserver for all of them), are attached as
soon as the document has a root, and get their CSP check then; a style cleared
while waiting is never attached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mode is a starting point, never a lock and never applied again on its own. modeStatus() (runtime)
compares the flags with the mode's preset: custom, what re-applying turns on and off, and which of
those the preset gained in an update since it was applied. mode_set keeps the preset of that moment
(modes.json "preset", flag names only, bounded; forgotten when the mode changes). Settings, the
first-run screen and the app menu's Mode row show Custom; Re-apply lists what it turns on and off
before it writes anything; a preset that grew is mentioned once on the mode row (Review), never applied.
Tests: exact match, changed by the owner, preset grew, choice made before the preset was kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+F pressed in a deck column (or in the shell while a deck is in front, for its focused column) opens that column's own find bar, laid over the column's header; F3 and Shift+F3 step through it. Same bar, same `tab_find`.
The app menu closed on any window blur. Taking the keyboard from a focused tab (Alt+F / F10 forwarded by the tab, then `shell_focus`) can pass through a blur on its way to the shell, which closed the menu a few milliseconds after it opened. It now closes on blur only if the focus is still away a moment later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The site adapter's staticCss(choice, mode) is written to its own <style> (wb-theme-static), rebuilt
only when the theme or the site's mode changes or on refresh(); the palette pass never reads it back
as the site's rules, and the first-frame snapshot carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generic (publisher, id, name, version, installer URLs and sha256 or local files, silent switches, licence, homepage). winget: version, installer and locale manifests (validated with the installed winget), Chocolatey: nuspec + install script with checksum (choco pack), Homebrew: a cask for our own tap, AUR: PKGBUILD and .SRCINFO from the .deb or AppImage. waybetter release windows|macos|linux writes them to <outDir>/distribution/<version> once the public download URL is given (--download-base); nothing is submitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
git archive stamps files with the commit time, older than the cached build in the shared target dir: a changed source looked unchanged and the build reused stale objects (seen as a missing module after a pointer bump).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app menu's "Report a problem…" opens a small dialog with the summary (reportModel.ts, English on purpose: the people who read reports do): the app and framework versions, the system and webview (coarse, from the user agent), the language and how it was chosen, the mode, the theme and icon pack ids, the file names of the plugins that are on, what paused itself (the panel's `reportDetails`), and safe mode. The model has no field for an account, a handle, an address, a tab or a path, and a value that is not made of id characters is left out rather than trimmed into shape, so nothing of that kind can ride along (tested with hostile input). The dialog sends nothing: Copy puts the text on the clipboard, and "Open the issue page" opens `ShellSite.menu.report.issueUrl` bare in the system browser (the summary is never put in a URL). Without an issue page the button is off and the dialog says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release macos --remote and release linux --wsl export HEAD plus each submodule at its recorded commit (git archive, never the working tree, nothing fetched from origin), build it path-free through build-env with unsigned bundles, bring the artifacts back and scan them here. release linux --appimage/--deb signs the AppImage (the Linux update payload) on the release machine and adds linux-x86_64 to latest.json. The remote script is low priority, never installs, quits or replaces an app on the Mac. Config: prepareCommand, exportExclude.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The renamed app may start its numbering again. Told apart by DisplayName in the uninstall key
(the old app wrote its old name there; this template writes the new one). Also exempt from the
silent-downgrade abort. Compiled with and without a legacy name and run on the test variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A theme may now say how each piece of a site is built, not only one radius and one density:
- components (themeComponents.ts): 36 generic parts with clamped tokens (radius, border incl.
98.css-style bevels, fill by color role / glass / gradient, padding, sizes, type, shadow, shape,
hover/active/focus), presets and parents; compiled over a site's part map as soft values
(--wb-part-*) and rigid rules; text on a chosen fill keeps its contrast.
- variants (themeVariants.ts): fixed words on six axes; the owner holds an axis or switches layout
changes off; the look of a variant is the site's skin.
- decoration (themeDecor.ts): dividers, chrome (title bar, masthead, prompt), a focus ring that is
always visible, selection, scrollbars, links, motion under reduced-motion.
- icon packs (themeIcons.ts): a standalone document of glyphs by semantic name (path data, colored
layers or a small PNG, never an SVG document), with license, source and credits; a theme only
names a pack; the owner's icon choice is separate; drawn as CSS masks of the current color over
the site's own svg boxes, behind forced-colors and mask-support guards, per group.
- a theme document gains the optional groups components, variants, decor, icons and
colors.actions; a theme without them is sanitized to exactly what it was.
- theme.schema.json and the new icons.schema.json are generated from the same tables
(scripts/theme-schema.mjs; a test fails when they are stale).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host: `safe.rs` is a flag in this process's memory, never written to disk, so the next normal start is back to the owner's setup. While it is on a site tab gets the tab bridge and nothing of the app's page layers: its document-start scripts are `SiteHooks::safe_init_scripts` (default: the bridge alone) instead of `init_scripts`; `site_page_loaded`, `tab_placed` and `tab_background` are not called; `page_scripts` is empty; `eval_in_site_tabs` does nothing. So no page runtime, plugins, theme, custom CSS, feature flags, fixes, hotfix data or words reach a page: the site runs as in a browser, and only the shell is the app's. It starts from the launch argument `--safe-mode` (for when the interface itself is broken), from Shift held while the app starts (Windows), or from the shell: `safe_mode_get` / `safe_mode_set {on}` (shell-only), where a switch rebuilds every tab webview (profiles marked stale, `tabs-rebuild` asks the shell for its layout) so document-start scripts change too. Tests: the argument, the flag, and that app.rs and host.rs gate every layer.
Frame: the app menu's "Restart in safe mode…" says what it does before anything reloads; while on, the titlebar shows an amber "Safe mode | Leave" chip (one click leaves) and the row reads "Leave safe mode"; the chrome drops its theme (and pane themes) and takes it back on leaving, without touching the first-frame cache. `features.safeMode` (on by default).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keys and saved data unchanged. Each language reuses its own word for a deck; only "Deck settings"
is newly translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A site may color an icon or text with one of its border grays (X's composer toolbar: 1.7:1 on its
own black). Placed by axis position it landed between a theme's background and its secondary
text, under every contrast a theme guarantees. In text usage that stretch of the axis (above the
most raised surface, below secondary text) now maps to the theme's secondary text; surface-dark
foregrounds (labels on a filled surface), borders and backgrounds are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Host: `updater::check_now` runs the same check the timer runs (the same updater endpoint, nothing else) for the app's shell-only `update_check`. It never queues or retries: while a check is in flight (the timer's or an earlier request's), or within 10 seconds of the last request, it only answers with the current status (`update::may_check_now`, tested). The timer's loop uses the same single-flight guard.
Frame: the menu's Updates row is that check (`updateRow`, tested): "Checking…", "Up to date · <version>", "Downloading <version>…", "Restart to update" when an update is ready (the row then installs it), and "Could not check" with a plain sentence as its tooltip when the server does not answer. The updater's error text is never shown there, and a failure is only mentioned when the owner asked for the check in this menu; the timer's own failures stay quiet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>