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).
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).
Seen live on the shared app: "domain" on example.com showed "2/2" for the first of two matches, and "2 matches" after Next. `ICoreWebView2Find::ActiveMatchIndex` starts at 1 (-1: none); `FindState::from_webview2` converts it (tested), so the bar reads 1/2, then 2/2.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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).
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).
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.
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.
Host: `tab_find {id, action, query}` (shell-only; start, next, previous, stop). On Windows it drives WebView2's own find (`ICoreWebView2Find` from `ICoreWebView2_28`, find options from `ICoreWebView2Environment15`; its built-in find bar suppressed, all matches highlighted) and reports the engine's match count and active match to the shell as `tab-find` events. On macOS, Linux and an older WebView2 Runtime native code evaluates the engine's `window.find` in the tab instead (next and previous work, no count); WKWebView's and WebKitGTK's native find are not wired here because this tree cannot be compiled or run for them on this machine. The query is one typed line (1..=256 characters, no control characters), goes to the engine and is kept nowhere. Tabs gain no command: a tab forwards the existing key name `find` (Ctrl+F) through tab_shortcut.
Frame: FindBar.tsx takes the address field's place in its pane's toolbar while open, so the page does not move and in split view the bar is the searched pane's. "3/17" where the engine counts, "No matches" for none; Enter / F3 next, Shift+Enter / Shift+F3 previous, Esc closes, clears the highlights and gives the page the keyboard. `features.find` (on by default) gates the menu row, the palette entry and the keys. Only what the page has rendered can be found, as in a browser.
Also: this makes `pub mod find` in lib.rs (already in HEAD) resolve again.
The logo at the start of the titlebar is now a button (aria-haspopup, keyboard reachable, tooltip) and a second trigger of the same menu: same component and model, only the anchor differs (`openAppMenu({ anchor: 'logo' })`). A submenu opens toward the middle of the window, else on the other side, else over the menu (`submenuLeft`, tested). A click on the logo never starts a window drag; a press that moves more than 5px does (the window's `startDragging`), so the logo still moves the window like the titlebar around it. A right-click shows the window's system menu. Clear of the macOS traffic lights: the logo sits after the titlebar's 78px inset and the menu hangs below it. The button at the end and the account menu's row stay.
The entry opens the notes in the page; its keywords carry the paused flags of that area. English
for the three new words; the other catalogs follow with the words step.
Frame: AppMenu.tsx is a menu at the end of the titlebar cluster (before the caption buttons; macOS has none there). Its rows are `ShellSite.menu`: ordered groups of built-in rows by name and the app's own ({id, label, icon, shortcut, hint, when, checked, run | submenu}); `DEFAULT_MENU`, `menuWith` and `menuWithout` order, add and remove. A built-in row shows only when the app serves it (decks, `menu.languages`, themes, `menu.modes`, and `features`: zoom, store, workspaces, developer, updates), so no app gets a dead row. Language, Theme, Mode, Workspaces and Developer are submenus; Language and Theme apply live and keep the menu open, Mode asks the same keep-my-changes question as Settings (panel `menuModes()`). Keyboard: arrows, Home/End, type-ahead, the arrow along the reading direction opens a submenu, Esc closes one level; ARIA menu roles; submenus stay inside the window; one highlight for pointer and keys. It is an overlay like the other shell menus (the shell is raised, panes stay visible, nothing is dimmed).
Words: menuMessages.en.json (`wb.menu.*`), read through the framework i18n with that English as the fallback until the catalogs have them; words the panel already has are reused.
Host: `tab_zoom {id, factor}` (shell-only) sets a tab's page zoom with the engine's own zoom, clamped to 25%..500% and remembered per tab id in app data `zoom.json` (a number per tab id, nothing else; a closed tab's entry goes); without a factor it reads. A tab may forward four more shortcut names: `app-menu`, `zoom-in`, `zoom-out`, `zoom-reset`. The titlebar shows a zoom chip only while the pane in front is not at 100%.
create-waybetter: `tab_zoom` in the shell capability list; the template says which panel sections it lacks (store, updates) and how to add menu rows.
- create-waybetter: the generated app has a feature table (one area, one feature, three modes), a feature module, the registry in its runtime, the cards and the first-run screen in its panel
- waybetter i18n rename --map <file> [--copy]: keys move or are copied in every catalog with each language's own value
- feature context: win (no globals in a module), text() for a feature's words (t('...') is read as a whole catalog key by the i18n tools)
- probes: `when` (only counts while the part of the page it lives in is there)
- cards: a caret, an empty area says its features come later; the first-run screen takes the whole panel
- PanelSite.features: the app's feature table; AreaPlugins draws one card per area (switch, count, grouped features, Settings button that runs the area's command in the site tab) and the core flags as Fixes
- a feature still backed by an old plugin file is switched through plugin_set_enabled from its card and left out of the JavaScript plugins list
- Modes: Onboarding (three cards, recommended preselected; an existing setup may be kept) and ModeSettings with the keep-my-changes question; openOnboardingWhenDue for ShellSite.start
- flags are saved sparsely (only what differs from the table's defaults)
- PanelSite.appearance.extra; palette commands of an area are listed under its name
- the kit's words for feature settings and health notes; English in the framework catalog
- settings: MAX_FEATURES 64 -> 256 (an older app's whole-map backup still validates)
- modes: ModeState {mode, onboarded, existing} in modes.json; existing is decided natively on the first start
- plugins::migrate: PluginHost.superseded (file -> flag); enabled and as shipped -> flag on and the file goes; edited -> kept, flag left off; marker written last, every step repeatable; map_backup for imported enabled lists
- a superseded file is never written again by install_bundled
- create-waybetter: the host command list and PluginHost.superseded
- featureTable.ts: areas, per-feature flags, core flags, modes as presets; sparse storage, ownership (an area that is off turns off what it owns), mode switching that keeps or drops manual changes, table validation
- featureModules.ts: createFeatureRegistry (flag + surface gating, scoped context, palette commands under the area with a budget of 5, settings dialog, probes, hotfix selector overrides and kill flags)
- settingsForm.ts: a settings schema normalizes stored values and draws the controls from the UI kit
- domHub observeSelector; api.observe goes through the shared hub (a root of the caller's own keeps its observer)
- createFeatureFlags: a delivery is the whole state (a flag it does not name is at its default), resolve() for ownership, current()
A library a build script compiles with MSVC (CMake, the cc crate) kept its
absolute source paths through __FILE__: -ffile-prefix-map exists only on gcc
and clang. cl.exe and link.exe read CL and LINK themselves, so the roots are
trimmed whatever runs the compiler. Longest root first: the first match wins.
wayBetter for X took My Space (profile customization) out; the framework only named it in
comments as an example of an app's own section and runtime member.
A Rust binary embeds the absolute paths of its sources, and on a developer's machine those hold
the account name. waybetter build-env runs a build with the home folder, the workspace, CARGO_HOME,
RUSTUP_HOME and CARGO_TARGET_DIR remapped to /home, /src, /cargo, /rustup and /target (flags already
set are kept), a path-free PDB reference on MSVC and the same mapping for C sources on gcc and clang.
waybetter privacy-scan searches files, binaries (UTF-8, UTF-16), archives (zip, tar, gzip, zstd,
.deb, DMG; NSIS, AppImage and xz through 7-Zip or the matching tool), git-tracked files and a whole
history for the terms of a file kept outside every repository plus the machine's account name. Hits
are printed masked; a container that cannot be opened is a failure.
waybetter release windows builds with build-env; both release commands refuse an artifact with a hit.
The scaffolded app gets desktop:build and privacy:scan scripts.
Automatic language choices resolve only to supported languages; community
translations are listed apart in Settings and offered as a one-click switch.
waybetter i18n check reads `supported` and generates `tier` into languages.json.
Moved from wayBetter for X: 27 messages in 51 languages, and windows/languages/<Language>.nsh for the 17 NSIS languages Tauri does not ship (scripts/installer-languages.mjs writes and checks them).