From 6d8eaaecd32f5e142f3a92358e5621889ff39dc2 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:17:57 +0700
Subject: [PATCH 01/11] fix(frontend): install the @pathscale/ui the code was
migrated to
The suite failed 65 tests across 11 files on a clean master. The cause was
not the app code: package.json asked for ^2.2.2 and bun.lock resolved 2.2.2,
but node_modules held 1.3.1, a major version behind.
v1 exports Modal, Toggle and EmptyState where v2 exports Dialog, Switch and
Empty, so three of the twelve components this app imports did not exist at
runtime. CloseConfirm and WelcomeFlow crashed reading a property of an
undefined Dialog, and the other 47 failures were the 5s timeout that follows
when a component never renders and findByRole never resolves.
Move to 2.5.0, published and the version the owner asked for. No source
change was needed, which is the evidence the migration itself was sound:
563 tests pass, 56 of 56 files, and tsc is clean. Suite time drops from
117s to 18s now that nothing waits on a component that cannot mount.
---
Cargo.lock | 12 ++++++------
Cargo.toml | 2 +-
apps/gui/frontend/bun.lock | 4 ++--
apps/gui/frontend/package.json | 2 +-
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 8bc21bf8..2020e9e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,7 +72,7 @@ dependencies = [
[[package]]
name = "agency-tools"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"derive_more 2.1.1",
"dirs",
@@ -364,11 +364,11 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "az-core"
-version = "0.8.2"
+version = "0.8.3"
[[package]]
name = "az-gui"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"agency-proxy-client",
"agency-proxy-protocol",
@@ -407,7 +407,7 @@ dependencies = [
[[package]]
name = "az-mcp-proxy"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"az-core",
]
@@ -498,7 +498,7 @@ dependencies = [
[[package]]
name = "blitz-bench"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"blitz-control-protocol",
"endpoint-libs",
@@ -8978,7 +8978,7 @@ dependencies = [
[[package]]
name = "wt-migrate"
-version = "0.8.2"
+version = "0.8.3"
dependencies = [
"chrono",
"derive_more 2.1.1",
diff --git a/Cargo.toml b/Cargo.toml
index 75b6c875..f2e51413 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ members = [
]
[workspace.package]
-version = "0.8.2"
+version = "0.8.3"
edition = "2024"
publish = false
diff --git a/apps/gui/frontend/bun.lock b/apps/gui/frontend/bun.lock
index 0b592bb5..107ec4a0 100644
--- a/apps/gui/frontend/bun.lock
+++ b/apps/gui/frontend/bun.lock
@@ -6,7 +6,7 @@
"name": "az-gui-frontend",
"dependencies": {
"@iconify/tailwind4": "^1.0.6",
- "@pathscale/ui": "^2.2.2",
+ "@pathscale/ui": "^2.5.0",
"@tauri-apps/api": "^2.1.1",
"clsx": "^2.1.1",
"popmotion": "^11.0.5",
@@ -192,7 +192,7 @@
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
- "@pathscale/ui": ["@pathscale/ui@2.2.2", "", { "dependencies": { "@tanstack/solid-virtual": "^3.13.27", "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@solid-primitives/event-listener": "^2.3.0", "@solid-primitives/intersection-observer": "^2.1.3", "@solid-primitives/keyboard": "^1.2.5", "@solid-primitives/media": "^2.2.5", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.22", "@solid-primitives/scheduled": "^1.4.1", "@solid-primitives/scroll": "^2.0.20", "@solid-primitives/storage": "^2.1.1", "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", "@tanstack/solid-form": "^1.29.0", "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-Lv8wVYlnWe1+ggI89NGnOHs3QXt0H1rbxD+hCdjEaRVeOMGbKzxXbex8OK5KpLY3dWV9cA0Q0oxY8Q3Y7eFVHQ=="],
+ "@pathscale/ui": ["@pathscale/ui@2.5.0", "", { "dependencies": { "@tanstack/solid-virtual": "^3.13.27", "clsx": "^2.1.1", "tailwind-merge": "^3.6.0" }, "peerDependencies": { "@solid-primitives/event-listener": "^2.3.0", "@solid-primitives/intersection-observer": "^2.1.3", "@solid-primitives/keyboard": "^1.2.5", "@solid-primitives/media": "^2.2.5", "@solid-primitives/props": "^3.1.8", "@solid-primitives/resize-observer": "^2.0.22", "@solid-primitives/scheduled": "^1.4.1", "@solid-primitives/scroll": "^2.0.20", "@solid-primitives/storage": "^2.1.1", "@solid-primitives/utils": "^6.2.1", "@standard-schema/spec": "^1.0.0", "@tanstack/solid-form": "^1.29.0", "@tanstack/solid-table": "^8.0.0", "popmotion": "^11.0.5", "solid-js": "^1.9", "solid-layouts": "^0.1.3" }, "optionalPeers": ["@standard-schema/spec", "popmotion"] }, "sha512-CjtKk9nFZRlKjQ15W6F+6aUm77Te5XN5JnxAsNIgSTPL7Owk/NiMl4sEc2msxflzv5wCTOdeJIXvJqph0BfbIg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
diff --git a/apps/gui/frontend/package.json b/apps/gui/frontend/package.json
index 10e50994..0e1d6e70 100644
--- a/apps/gui/frontend/package.json
+++ b/apps/gui/frontend/package.json
@@ -20,7 +20,7 @@
"license": "MIT",
"dependencies": {
"@iconify/tailwind4": "^1.0.6",
- "@pathscale/ui": "^2.2.2",
+ "@pathscale/ui": "^2.5.0",
"@tauri-apps/api": "^2.1.1",
"clsx": "^2.1.1",
"popmotion": "^11.0.5",
From 6901add5ede96f5669d6920d5d376d2b57949b8e Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:23:05 +0700
Subject: [PATCH 02/11] fix(delivery): stop the verify gate asking for two
runtimes at once
`stable` could not build. The rust gate ran clippy and the tests under
`--all-features`, and this workspace is one where that flag cannot be used:
`blitz-runtime` and `webview-runtime` are mutually exclusive, and `main.rs`
turns enabling both into a compile error naming them. The gate therefore
failed before linting anything, so no delivery bundle could be produced.
Name the runtime that ships instead, with the inspector `stable` already
builds the binary against, which keeps the gate checking the code that
actually goes out.
---
scripts/local-delivery.sh | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/scripts/local-delivery.sh b/scripts/local-delivery.sh
index 64cc97c7..12cd229c 100755
--- a/scripts/local-delivery.sh
+++ b/scripts/local-delivery.sh
@@ -112,12 +112,17 @@ if [ "$mode" != "quick" ]; then
(
cd "$repo_root"
cargo fmt --all --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
+ # Not `--all-features`. `blitz-runtime` and `webview-runtime` are mutually
+ # exclusive, and `apps/gui/src/main.rs` makes enabling both a compile error,
+ # so the flag that means "check everything" is the one flag this workspace
+ # cannot take: it fails before any lint or test runs. Name the runtime that
+ # ships, with the inspector `stable` builds against.
+ cargo clippy --workspace --all-targets --features az-gui/blitz-inspector -- -D warnings
# Store-backed tests open several files apiece. macOS's default descriptor
# ceiling can make the parallel harness fail with `Too many open files`,
# masking healthy assertions as a broken release. Serial execution adds less
# than a second here and keeps the delivery gate deterministic.
- RUST_TEST_THREADS=${RUST_TEST_THREADS:-1} cargo test --workspace --all-features
+ RUST_TEST_THREADS=${RUST_TEST_THREADS:-1} cargo test --workspace --features az-gui/blitz-inspector
)
fi
From be045a2309a12c5e3b955edb4ce0298f27fb2079 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:25:50 +0700
Subject: [PATCH 03/11] docs: stop describing a .cargo/config.toml that no
longer exists
The git workflow section still said the file is tracked, carries the macOS
link flag and the usvg patch, and that its working copy should equal its
commit. None of that has been true since e8c30f4: the patch had already moved
to the root manifest, that commit moved the linker flag into apps/gui/build.rs,
and the file was deleted. `.cargo/` now holds only the gitignored
local-renderer.toml.
An agent reading the old text goes looking for a tracked file that is not
there, and the natural repair is to recreate it, which is precisely the
arrangement the rest of the section exists to prevent.
---
AGENTS.md | 27 +++++++++++++++------------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 2d39da2a..77e2a376 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -104,9 +104,9 @@ paraphrases were how the old checkbox contract created near-duplicates. Full con
scripts/local-renderer.sh check -p az-gui --features blitz-runtime
```
- It used to be a block inside `.cargo/config.toml`, defended by
- `git update-index --skip-worktree`. That failed four ways at once. The file is
- tracked, because it carries the macOS link flags and the `usvg` patch every
+ It used to be a block inside a tracked `.cargo/config.toml`, defended by
+ `git update-index --skip-worktree`. That failed four ways at once. The file was
+ tracked, because it carried the macOS link flag and the `usvg` patch every
build needs, so `.gitignore` could not protect it and the paths went in four
times. `skip-worktree` hid the file from `git status` as well and blocked
branch switches. The redirect was always on, so no ordinary build ever fetched
@@ -115,15 +115,18 @@ paraphrases were how the old checkbox contract created near-duplicates. Full con
`Cargo.lock`, replacing git revisions with paths, which reached a commit twice.
The wrapper snapshots and restores the lockfile around the build, so none of
- that is left to remember. `.cargo/config.toml` needs no flag now: its working
- copy should equal its commit, and `git status` will say so if it does not.
-
- It is also down to one setting, the macOS linker flag, which is the only thing
- in it that cannot live in a manifest. The usvg patch moved to the root
- `Cargo.toml`, where patches belong and where ps-blitz keeps the same one. That
- it ever sat in `.cargo/config.toml` is the whole story: a shared patch shared a
- `[patch.crates-io]` table with paths that existed on one machine, which is how
- a file nobody could delete became a file nobody could safely commit.
+ that is left to remember.
+
+ **There is no `.cargo/config.toml` any more**, and nothing should recreate one:
+ `.cargo/` holds only the gitignored `local-renderer.toml`. Both of its former
+ settings found a better home. The usvg patch moved to the root `Cargo.toml`,
+ where patches belong and where ps-blitz keeps the same one. The macOS linker
+ flag moved into [`apps/gui/build.rs`](apps/gui/build.rs), which emits
+ `cargo::rustc-link-arg-bins` for the one binary that wants it rather than for
+ every crate in the workspace. That the two ever shared a file is the whole
+ story: a shared patch shared a `[patch.crates-io]` table with paths that
+ existed on one machine, which is how a file nobody could delete became a file
+ nobody could safely commit.
Pins still want checking before a release, because the opt-in path skips them
by definition. `scripts/check-one-rev-per-git-source.sh` refuses a lockfile
From 23111b85eca13ef821defc7f2e25b97112db6a75 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:30:29 +0700
Subject: [PATCH 04/11] refactor(frontend): use the library Flex where the
classes were exactly it
`flex items-center gap-2` was written out at 17 call sites, the most repeated
layout signature in the app. The library's own recipe maps `gap="sm"` to
`gap-2` and `align="center"` to `items-center`, and its base class is nothing
but `display: flex`, so `` emits the same three
classes on the same `div`. The rendering is unchanged by construction.
Only this exact signature. The neighbouring ones are deliberately left alone:
`flex items-center gap-2 pt-0.5` and `flex items-baseline gap-2` carry padding
and alignment the recipe has no parameter for, and inventing one to absorb
them would move a decision out of the call site that reads it.
---
apps/gui/frontend/src/App.tsx | 5 +--
.../src/features/onboarding/WelcomeFlow.tsx | 10 +++---
.../src/features/project/ProjectPanel.tsx | 10 +++---
.../src/features/project/TranscriptPane.tsx | 10 +++---
.../src/features/settings/SettingsTab.tsx | 34 +++++++++----------
.../src/features/settings/ThemePicker.tsx | 10 +++---
6 files changed, 40 insertions(+), 39 deletions(-)
diff --git a/apps/gui/frontend/src/App.tsx b/apps/gui/frontend/src/App.tsx
index 1c1a5509..3c9e657e 100644
--- a/apps/gui/frontend/src/App.tsx
+++ b/apps/gui/frontend/src/App.tsx
@@ -1,3 +1,4 @@
+import { Flex } from "@pathscale/ui";
import {
createEffect,
createMemo,
@@ -278,7 +279,7 @@ export function BootFailed(props: {
{props.message}
-
+
-
+
);
}
diff --git a/apps/gui/frontend/src/features/onboarding/WelcomeFlow.tsx b/apps/gui/frontend/src/features/onboarding/WelcomeFlow.tsx
index c09369d8..0d8511b7 100644
--- a/apps/gui/frontend/src/features/onboarding/WelcomeFlow.tsx
+++ b/apps/gui/frontend/src/features/onboarding/WelcomeFlow.tsx
@@ -1,4 +1,4 @@
-import { Dialog, Select } from "@pathscale/ui";
+import { Dialog, Flex, Select } from "@pathscale/ui";
import { createEffect, createMemo, createSignal, For, type JSX, Show } from "solid-js";
import { Button } from "~/components/Button";
import { Icon, type IconProps } from "~/components/Icon";
@@ -264,7 +264,7 @@ export function WelcomeFlow(): JSX.Element {
{state.workspaceRoot?.path ?? tx("Not available")}
-
+
+
@@ -649,7 +649,7 @@ export function WelcomeFlow(): JSX.Element {
-
+
);
}
@@ -2536,7 +2536,7 @@ function InternalPerformance(): JSX.Element {
label={tx("Measurements")}
hint={tx("since this window opened, or since the last reset")}
>
-
+
{/*
Stacked: a full-width table beside a `flex-1` label squeezes the label to
diff --git a/apps/gui/frontend/src/features/settings/ThemePicker.tsx b/apps/gui/frontend/src/features/settings/ThemePicker.tsx
index c84b2939..0318fb15 100644
--- a/apps/gui/frontend/src/features/settings/ThemePicker.tsx
+++ b/apps/gui/frontend/src/features/settings/ThemePicker.tsx
@@ -1,4 +1,4 @@
-import { Radio } from "@pathscale/ui";
+import { Flex, Radio } from "@pathscale/ui";
import { createEffect, For, type JSX, Show } from "solid-js";
import { Button } from "~/components/Button";
import {
@@ -239,7 +239,7 @@ function AccentSelector(props: {
{t("appearance.accentColourHint")}
-
+
);
}
From 4127ee34a91cf75eb2607480c727083d7b81d5f4 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:41:56 +0700
Subject: [PATCH 05/11] fix(clipboard): answer paste ourselves, and let the
petals show their colour
Two 2.x-era breakages found by driving the shipping Blitz build.
Paste: the renderer dispatches no `paste` event to JS at all, and answers the
chord itself only for a focused native text input. Anything else reached no
handler in either language, so pasting did nothing. Cmd/Ctrl+V is now served
next to Copy, which was already here for the same reason. It writes through
the field and dispatches `input`, because a controlled Solid field takes its
value from a signal that only that event updates: without it the character
count, the autosize and the submitted text all keep the pre-paste value.
`execCommand("paste")` is deliberately not a fallback; scripted reads are
refused everywhere, so a refusal is the real answer and the field is left be.
The colour wheel: 2.x gives `.radio__control` its own
`background-color: var(--color-base-100)`, and the petal's colour is the
indicator *inside* that control. The fill is painted over it, so all 31 petals
render as identical empty circles. The override list already restated margin,
size, border width and border colour for this control; it never had to restate
the background, because the v1 control had no fill. Measured on the real
renderer: the petals were always the right size in the right circle, which is
why this reads as a colour bug rather than a layout one.
---
.../src/features/project/MessageBody.tsx | 24 +++
.../src/features/settings/ThemePicker.tsx | 12 +-
.../src/features/tabs/shortcuts.test.tsx | 148 ++++++++++++++++++
.../frontend/src/features/tabs/shortcuts.ts | 41 ++++-
4 files changed, 223 insertions(+), 2 deletions(-)
create mode 100644 apps/gui/frontend/src/features/tabs/shortcuts.test.tsx
diff --git a/apps/gui/frontend/src/features/project/MessageBody.tsx b/apps/gui/frontend/src/features/project/MessageBody.tsx
index a9ec2064..9d4d9f74 100644
--- a/apps/gui/frontend/src/features/project/MessageBody.tsx
+++ b/apps/gui/frontend/src/features/project/MessageBody.tsx
@@ -58,6 +58,30 @@ export async function copyText(text: string): Promise {
return ok;
}
+/**
+ * Read the clipboard, by whichever route works here.
+ *
+ * The mirror of {@link copyText}, and it exists for the same reason that one
+ * does: the renderer that ships is Blitz, not WebKit, and it dispatches no
+ * `paste` event to JS at all. Blitz answers ⌘V itself, but only for a focused
+ * native text input (`blitz-dom`'s `text.rs`), so a paste aimed at anything
+ * else reached no handler in either language and did nothing at all.
+ *
+ * `execCommand("paste")` is deliberately not a fallback. It is refused for
+ * scripted callers everywhere for the obvious reason — a page that can read the
+ * clipboard unprompted can read your password manager — so there is nothing to
+ * fall back *to*. A refusal here is the real answer, and the caller leaves the
+ * field alone rather than clearing it.
+ */
+export async function pasteText(): Promise {
+ try {
+ return await navigator.clipboard.readText();
+ } catch (cause) {
+ log.warn(`the clipboard could not be read: ${describeError(cause)}`);
+ return null;
+ }
+}
+
/**
* Enough markdown for what the agent actually emits: paragraphs, `**bold**`,
* `` `code` `` and fenced blocks.
diff --git a/apps/gui/frontend/src/features/settings/ThemePicker.tsx b/apps/gui/frontend/src/features/settings/ThemePicker.tsx
index 0318fb15..6388c1c2 100644
--- a/apps/gui/frontend/src/features/settings/ThemePicker.tsx
+++ b/apps/gui/frontend/src/features/settings/ThemePicker.tsx
@@ -192,7 +192,17 @@ function SurfaceColorWheel(props: { value: string; onPick: (value: string) => vo
style={{ "background-color": color }}
/>
}
- class="size-7 cursor-pointer rounded-full [&_[data-slot=radio-control]]:m-0 [&_[data-slot=radio-control]]:size-7 [&_[data-slot=radio-control]]:border-2 [&_[data-slot=radio-control]]:border-az-hairline-strong [&_[data-slot=radio-indicator]]:overflow-hidden [&_[data-slot=radio-indicator]]:rounded-full"
+ /*
+ * `bg-transparent` on the control is load-bearing, not tidying.
+ *
+ * The petal's colour is the indicator, and the control is the
+ * box drawn around it. 2.x gives that box its own
+ * `background-color: var(--color-base-100)`, which is painted
+ * over the indicator and turns all 31 petals into identical
+ * empty circles. The v1 control had no fill, so this override
+ * was not needed and its absence was invisible.
+ */
+ class="size-7 cursor-pointer rounded-full [&_[data-slot=radio-control]]:m-0 [&_[data-slot=radio-control]]:size-7 [&_[data-slot=radio-control]]:border-2 [&_[data-slot=radio-control]]:border-az-hairline-strong [&_[data-slot=radio-control]]:bg-transparent [&_[data-slot=radio-indicator]]:overflow-hidden [&_[data-slot=radio-indicator]]:rounded-full"
/>
);
diff --git a/apps/gui/frontend/src/features/tabs/shortcuts.test.tsx b/apps/gui/frontend/src/features/tabs/shortcuts.test.tsx
new file mode 100644
index 00000000..15e8e989
--- /dev/null
+++ b/apps/gui/frontend/src/features/tabs/shortcuts.test.tsx
@@ -0,0 +1,148 @@
+import { createRoot } from "solid-js";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { useTabShortcuts } from "~/features/tabs/shortcuts";
+
+/**
+ * The clipboard chords, which the shipping renderer does not deliver as events.
+ *
+ * Blitz dispatches no `copy` or `paste` event to JS, so both are answered from
+ * a plain `keydown` here. These tests drive that keydown directly, which is
+ * exactly the surface the renderer does provide.
+ */
+
+const openDraft = vi.fn();
+const cycleTab = vi.fn();
+
+vi.mock("~/stores/workspace", () => ({
+ useWorkspace: () => ({ actions: { openDraft, cycleTab } }),
+}));
+
+const writeText = vi.fn(() => Promise.resolve());
+const readText = vi.fn(() => Promise.resolve(""));
+
+vi.mock("~/lib/platform", () => ({ isTauri: () => false, isBlitz: () => false }));
+
+function mount(): () => void {
+ let dispose = (): void => {};
+ createRoot((disposer) => {
+ dispose = disposer;
+ useTabShortcuts();
+ });
+ return dispose;
+}
+
+/** Mounting runs `onMount`, which is what installs the window listener. */
+function press(key: string, target: EventTarget): void {
+ target.dispatchEvent(
+ new KeyboardEvent("keydown", {
+ key,
+ code: `Key${key.toUpperCase()}`,
+ metaKey: true,
+ bubbles: true,
+ }),
+ );
+}
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ Object.defineProperty(navigator, "clipboard", {
+ configurable: true,
+ value: { writeText, readText },
+ });
+});
+
+describe("paste", () => {
+ it("inserts the clipboard at the caret and tells the field it changed", async () => {
+ readText.mockResolvedValueOnce("world");
+ const field = document.createElement("textarea");
+ field.value = "hello ";
+ field.setSelectionRange(6, 6);
+ document.body.append(field);
+ const onInput = vi.fn();
+ field.addEventListener("input", onInput);
+
+ const dispose = mount();
+ press("v", field);
+ await vi.waitFor(() => expect(field.value).toBe("hello world"));
+
+ // A controlled Solid field reads its value from a signal that only an
+ // `input` event updates, so without this the paste is invisible to the app.
+ expect(onInput).toHaveBeenCalled();
+ expect(field.selectionStart).toBe("hello world".length);
+
+ dispose();
+ field.remove();
+ });
+
+ it("replaces the selection rather than inserting beside it", async () => {
+ readText.mockResolvedValueOnce("there");
+ const field = document.createElement("input");
+ field.value = "hi world";
+ field.setSelectionRange(3, 8);
+ document.body.append(field);
+
+ const dispose = mount();
+ press("v", field);
+ await vi.waitFor(() => expect(field.value).toBe("hi there"));
+
+ dispose();
+ field.remove();
+ });
+
+ it("leaves a read-only field alone and never reads the clipboard for it", () => {
+ const field = document.createElement("textarea");
+ field.value = "untouched";
+ field.readOnly = true;
+ document.body.append(field);
+
+ const dispose = mount();
+ press("v", field);
+
+ expect(readText).not.toHaveBeenCalled();
+ expect(field.value).toBe("untouched");
+
+ dispose();
+ field.remove();
+ });
+
+ it("does not read the clipboard when the paste lands outside a field", () => {
+ const dispose = mount();
+ press("v", document.body);
+
+ expect(readText).not.toHaveBeenCalled();
+
+ dispose();
+ });
+
+ it("keeps the field's text when the clipboard is refused", async () => {
+ readText.mockRejectedValueOnce(new Error("denied"));
+ const field = document.createElement("textarea");
+ field.value = "kept";
+ document.body.append(field);
+
+ const dispose = mount();
+ press("v", field);
+ await vi.waitFor(() => expect(readText).toHaveBeenCalled());
+
+ expect(field.value).toBe("kept");
+
+ dispose();
+ field.remove();
+ });
+});
+
+describe("copy", () => {
+ it("copies a field's selection rather than the whole value", async () => {
+ const field = document.createElement("textarea");
+ field.value = "one two three";
+ field.setSelectionRange(4, 7);
+ document.body.append(field);
+
+ const dispose = mount();
+ press("c", field);
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith("two"));
+
+ dispose();
+ field.remove();
+ });
+});
diff --git a/apps/gui/frontend/src/features/tabs/shortcuts.ts b/apps/gui/frontend/src/features/tabs/shortcuts.ts
index 615a515c..5368dbd5 100644
--- a/apps/gui/frontend/src/features/tabs/shortcuts.ts
+++ b/apps/gui/frontend/src/features/tabs/shortcuts.ts
@@ -1,5 +1,5 @@
import { onCleanup, onMount } from "solid-js";
-import { copyText } from "~/features/project/MessageBody";
+import { copyText, pasteText } from "~/features/project/MessageBody";
import { isBlitz, isTauri } from "~/lib/platform";
import { useWorkspace } from "~/stores/workspace";
@@ -68,6 +68,45 @@ export function useTabShortcuts(): void {
return;
}
+ /*
+ * Cmd+V and Ctrl+V into a text field, for the same reason Copy is here.
+ *
+ * Blitz dispatches no `paste` event to JS, and answers the chord itself
+ * only for a focused native text input. That covers the composer, so this
+ * is not where pasting into it comes from — but the native route writes
+ * straight into the editor's own buffer, and a controlled Solid field then
+ * holds a `value` the DOM no longer matches. Answering it here keeps the
+ * signal and the element in step.
+ *
+ * Only fields. A paste with nothing focused to receive it has no meaning,
+ * and reading the clipboard to discard it would be a permission prompt for
+ * nothing.
+ */
+ if ((event.metaKey || event.ctrlKey) && (event.key === "v" || event.code === "KeyV")) {
+ const target = event.target as HTMLElement | null;
+ const field =
+ target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement ? target : null;
+ if (!field || field.readOnly || field.disabled) return;
+
+ // Prevent the native insert, so text cannot land twice when the renderer
+ // also handles the chord.
+ event.preventDefault();
+
+ const from = field.selectionStart ?? field.value.length;
+ const to = field.selectionEnd ?? from;
+ void pasteText().then((text) => {
+ if (text === null || text.length === 0) return;
+ field.value = `${field.value.slice(0, from)}${text}${field.value.slice(to)}`;
+ const caret = from + text.length;
+ field.setSelectionRange(caret, caret);
+ // A controlled field takes its value from a signal that only an `input`
+ // event updates. Without this the character count, the autosize and the
+ // submitted text all keep the value from before the paste.
+ field.dispatchEvent(new Event("input", { bubbles: true }));
+ });
+ return;
+ }
+
if (event.altKey || event.shiftKey) return;
// ⌃T, the second way to open a new project. The native menu duplicates it
From e7e5fee592eaafd60496c0ab652cce4fca55f15c Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:49:32 +0700
Subject: [PATCH 06/11] fix(settings): persist a slider once, on release, not
on every pause
Dragging the glass sliders blanked the window. The cause is the one the
GlassAxis comment already describes: every persisted step is a `set_settings`
round trip costing 5-6ms on the window thread, and enough of them in a row
starve paint. Traced on the owner's live session while they reproduced it,
frames kept advancing (50243 -> 50317) while missed refreshes went 46 -> 73
and fps halved, and the profile log held 7,838 of those writes.
A 180ms debounce was standing in for the event that means "the knob was
released", because the pinned 1.3.1 had none. A debounce is not that event: it
fires whenever the pointer pauses, so a slow drag is still a stream of writes.
2.x carries `onChangeEnd`, and both call sites said in comments that they were
waiting for it, so `onChange` now paints and `onChangeEnd` persists.
The three cost-warning tests drove half an interaction: `keyDown` with no
`keyUp`, `pointerDown` with no `pointerUp`. That passed against a timer, which
needs no release, and could not pass against the real event. They now finish
the gesture, which is also what a user does. The library raises `onChangeEnd`
from `keyup` and `blur`, so holding an arrow key repeats the preview without
repeating the write.
---
.../features/settings/SettingsTab.test.tsx | 19 ++++--
.../src/features/settings/SettingsTab.tsx | 58 ++++++++++---------
2 files changed, 46 insertions(+), 31 deletions(-)
diff --git a/apps/gui/frontend/src/features/settings/SettingsTab.test.tsx b/apps/gui/frontend/src/features/settings/SettingsTab.test.tsx
index 182ed665..2d59400d 100644
--- a/apps/gui/frontend/src/features/settings/SettingsTab.test.tsx
+++ b/apps/gui/frontend/src/features/settings/SettingsTab.test.tsx
@@ -231,6 +231,11 @@ describe("cost warning settings", () => {
expect(slider).toHaveAttribute("aria-valuenow", "1");
expect(slider).toHaveAttribute("aria-valuetext", "$1.00");
+
+ // Releasing the key ends the keyboard interaction, which is what persists:
+ // the library raises `onChangeEnd` from `keyup` and `blur`, so holding an
+ // arrow down repeats the preview without repeating the store write.
+ fireEvent.keyUp(slider, { key: "ArrowRight" });
await waitFor(() => expect(screen.workspace.state.settings?.costWarningUsd).toBe(1));
});
@@ -264,12 +269,16 @@ describe("cost warning settings", () => {
fireEvent.pointerDown(track, { clientX: 100, pointerId: 1 });
- // Shown at once. The write waits for the knob to settle, so a drag does not
- // queue one serialized store round trip per tick.
+ // Shown at once, and not yet written: the store round trip belongs to the
+ // release, so a drag cannot queue one serialized write per tick.
expect(slider).toHaveAttribute("aria-valuenow", "10.25");
expect(slider).toHaveAttribute("aria-valuetext", "$10.25");
expect(screen.workspace.state.settings?.costWarningUsd).not.toBe(10.25);
+ // Releasing is what persists. The drag has to be finished for the value to
+ // reach the store at all, which is the whole point of `onChangeEnd`.
+ fireEvent.pointerUp(track, { clientX: 100, pointerId: 1 });
+
await waitFor(() => expect(screen.workspace.state.settings?.costWarningUsd).toBe(10.25), {
timeout: 2000,
});
@@ -301,13 +310,15 @@ describe("cost warning settings", () => {
() => new Promise((resolve) => releases.push(resolve)),
);
- // Two steps in quick succession. The display follows at once; the store
- // write waits for the knob to settle, so this is one save and not two.
+ // Two steps inside one keypress-and-release. The display follows each at
+ // once; the store write belongs to the release, so this is one save and
+ // not two.
fireEvent.keyDown(slider, { key: "ArrowRight" });
fireEvent.keyDown(slider, { key: "ArrowRight" });
expect(slider).toHaveAttribute("aria-valuenow", "1.25");
expect(releases).toHaveLength(0);
+ fireEvent.keyUp(slider, { key: "ArrowRight" });
await waitFor(() => expect(releases).toHaveLength(1), { timeout: 2000 });
// The preview is held until the save it belongs to lands, so a slow write
diff --git a/apps/gui/frontend/src/features/settings/SettingsTab.tsx b/apps/gui/frontend/src/features/settings/SettingsTab.tsx
index cdc1a029..f1d1c983 100644
--- a/apps/gui/frontend/src/features/settings/SettingsTab.tsx
+++ b/apps/gui/frontend/src/features/settings/SettingsTab.tsx
@@ -2421,13 +2421,12 @@ function CostSection(): JSX.Element {
step={0.25}
value={warningUsd()}
formatValue={(value) => `$${value.toFixed(2)}`}
- // Live while dragging, persisted when it settles. @pathscale/ui
- // 2.0.0 carries `onChangeEnd` for exactly this; 1.3.1, which is
- // what ships here, does not, so the settle is timed locally.
- onChange={(value) => {
- previewWarning(value);
- settleWarning(value);
- }}
+ // Live while dragging, persisted when the knob is released.
+ // `onChangeEnd` is the event that means exactly that, and it
+ // arrived with the 2.x line this app is now on; the local settle
+ // timer it replaces dated from the pinned 1.3.1, which had none.
+ onChange={(value) => previewWarning(value)}
+ onChangeEnd={(value) => settleWarning(value)}
size="sm"
class="w-full min-w-0 [&_[data-slot=label]]:sr-only"
/>
@@ -2760,15 +2759,18 @@ const SETTLE_MS = 180;
/**
* One appearance axis.
*
- * Dragging paints immediately and persists only when the drag settles. It used
- * to call `saveSettings` on every tick, and each of those awaited a serialized
- * store write before it even applied the CSS, then made a native window call:
- * one session's log held 75 `set_settings` and 76 `set_window_chrome` round
- * trips at 7 and 6ms, all on the window thread. That is why the knob led and
- * the panel lagged, and why dragging depth could starve paint until the window
- * went blank and only recovered once the drag stopped.
+ * Dragging paints immediately and persists once, when the knob is released. It
+ * used to call `saveSettings` on every tick, and each of those awaited a
+ * serialized store write before it even applied the CSS, then made a native
+ * window call: one session's log held 75 `set_settings` and 76
+ * `set_window_chrome` round trips at 7 and 6ms, all on the window thread. That
+ * is why the knob led and the panel lagged, and why dragging depth could starve
+ * paint until the window went blank and only recovered once the drag stopped.
*
- * The live/settled split is the slider's own now, so there is no timer here.
+ * A 180ms debounce replaced that and was a large improvement, but not a cure:
+ * a debounce fires whenever the pointer pauses, so a slow drag is still a
+ * stream of store writes. The live/settled split is the slider's own now
+ * (`onChange` paints, `onChangeEnd` persists), so there is no timer here.
*/
function GlassAxis(props: {
label: string;
@@ -2784,12 +2786,6 @@ function GlassAxis(props: {
onChange: (value: number) => void;
}): JSX.Element {
const toAxis = (value: number) => (props.toAxis ? props.toAxis(value) : value);
- let timer: ReturnType | undefined;
- onCleanup(() => clearTimeout(timer));
- const settle = (value: number): void => {
- clearTimeout(timer);
- timer = setTimeout(() => props.onChange(value), SETTLE_MS);
- };
return (
{
- writeGlassAxis(props.axis, toAxis(value));
- settle(value);
- }}
+ // Paint now: no store, no AppKit, no await.
+ onChange={(value) => writeGlassAxis(props.axis, toAxis(value))}
+ /*
+ * Persist once, when the knob is released.
+ *
+ * This used to be a 180ms debounce, because the pinned 1.3.1 had no
+ * `onChangeEnd` to hang it on. A debounce still fires mid-drag whenever
+ * the pointer pauses, and each of those is a `set_settings` round trip
+ * costing 5-6ms on the window thread: measured on the owner's live
+ * session, dragging this slider drove the profile log past 7,800 of
+ * them, paint starved, and the window blanked until the drag stopped.
+ * 2.5 has the real event, so the persist happens exactly once.
+ */
+ onChangeEnd={(value) => props.onChange(value)}
size="sm"
class="w-full min-w-0 [&_[data-slot=label]]:sr-only"
/>
From dcac9fbb5b853ff42f693c90b2e943f8451416e6 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 13:51:28 +0700
Subject: [PATCH 07/11] docs(glass): the renderer stopped being the blocker;
the switches still are
Two places said the shipping renderer cannot carry a blur. It can: vello gained
a real backdrop pass, `record_backdrop` with a blur sigma and an expansion
rect, and ce156b0 recorded that landing without revising the section that
contradicts it or the CSS comment repeating the same claim.
What actually keeps glass off is a pair of deliberate switches: the window
config carries no `transparent: true`, so `WINDOW_GLASS_ENABLED` stays false
and the effect view is never attached. They move together or not at all,
because an `NSGlassEffectView` over an opaque window flattens the whole app
under one colour, which is the thing that flag was added to prevent.
Stated as a table of three gates, with the two that are shut named by file and
line, so the next person reads a switch to open rather than a renderer to
rewrite.
---
apps/gui/frontend/src/styles/theme.css | 12 ++++++++++--
docs/glass-support.md | 26 +++++++++++++++++++++++---
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/apps/gui/frontend/src/styles/theme.css b/apps/gui/frontend/src/styles/theme.css
index 6769abac..c858539f 100644
--- a/apps/gui/frontend/src/styles/theme.css
+++ b/apps/gui/frontend/src/styles/theme.css
@@ -392,8 +392,16 @@
* only").
*
* The blur itself is the compositor's, through `NSGlassEffectView` behind a
- * transparent window. CSS `backdrop-filter` is not involved: it works in
- * `vello_cpu` but the shipping renderers cannot carry it yet.
+ * transparent window, and none of it is on yet: the window config carries
+ * no `transparent: true`, so `WINDOW_GLASS_ENABLED` in `lib/theme.ts` stays
+ * false and the effect view is never attached. Attaching it over an opaque
+ * window washes the whole app out under one flat colour, which is what that
+ * flag exists to prevent.
+ *
+ * CSS `backdrop-filter` is a separate route to the same look, and it is no
+ * longer the impossible one: the shipping vello backend gained a real
+ * backdrop pass (`record_backdrop` in `anyrender_vello/src/scene.rs`), so
+ * the claim that only `vello_cpu` can carry it is out of date.
*/
background-color: color-mix(
in oklab,
diff --git a/docs/glass-support.md b/docs/glass-support.md
index e7b68af6..0ce519be 100644
--- a/docs/glass-support.md
+++ b/docs/glass-support.md
@@ -77,10 +77,30 @@ The second can ship without the first and is where the existing groundwork is.
handful of properties and neither `outline` nor `box-shadow` is among them.
See `tests/blitz-tests/tests/panel_axes.rs`.
-### Not built: the blur, in the renderer that ships
+### Superseded 2026-08-16: the blur landed, the switches did not
-This is the whole remaining gap, and it is worth stating precisely because
-"backdrop-filter is implemented" and "the app cannot blur" are both true.
+The table below is kept because its reasoning is still useful, but its verdict
+is out of date. `anyrender_vello` — the backend that ships — now carries a real
+backdrop pass: `record_backdrop`, with a blur sigma and an expansion rect,
+reached from `push_layer` in
+[`anyrender_vello/src/scene.rs`](../../ps-anyrender/crates/anyrender_vello/src/scene.rs).
+`ce156b0` recorded that landing and this section was never revised to match.
+
+**So the remaining gap is not the renderer. It is that nothing turns glass on.**
+Three gates, and all of them are shut by choice:
+
+| gate | state | where |
+|---|---|---|
+| `transparent: true` on the window | **absent** | `apps/gui/tauri.conf.json` |
+| `WINDOW_GLASS_ENABLED` | **`false`** | `apps/gui/frontend/src/lib/theme.ts:290` |
+| renderer backdrop pass | **present** | `anyrender_vello/src/scene.rs` |
+
+The first two are a pair: attaching an `NSGlassEffectView` over an *opaque*
+window flattens every surface under one colour, which is what the flag is
+defending against, so neither moves without the other. Opening them is the
+feature landing this document plans, not a bug fix, and the risk note below
+still governs it — a black window is the shared failure mode of three separate
+stages, so change one thing at a time and confirm each.
| renderer | ships in `az-gui`? | filters |
|---|---|---|
From cc423b596f530a929623a2a19603a953c055ff74 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 14:11:33 +0700
Subject: [PATCH 08/11] feat(glass): drive panels from the library's three
glass numbers
Glass is meant to be core, so it should be the library's primitive rather than
a set of axes this app invented. It already is: 2.5.0 ships `styles/glass.ts`,
which derives twenty-five `--glass-*` tokens from `blur`, `refraction` and
`depth`, and its own doc comment credits the consulting.parcle.ai tuning
surface the owner pointed at. The port upstream was already done; what was
missing was a consumer.
So `applyTheme` now calls `applyGlassTokens` and Settings gains those three
sliders, with `GLASS_LIMITS` and `GLASS_DEFAULTS` supplying their ranges and
defaults so nothing is restated here and a retune upstream arrives for free.
The preview re-derives the whole set from the other two axes on every change:
three of the tokens are read by component CSS without a fallback, and an
undefined custom property drops the declaration rather than falling back, so a
partial set is a panel with no background at all.
`.az-glass` is what makes any of it visible. Nothing in this app rendered
`material="glass"`, so tokens alone would have been three controls that move
nothing, which is exactly what the comment they replace warned about. It
carries the same fallbacks, the same no-nested-blur rule and the same
reduced-transparency and unsupported-backdrop paths the library uses, and it
replaces the opaque fill rather than layering over it, because a backdrop
filter behind an opaque background blurs nothing.
The old axes stay. They style AgencyZero's own hue-ladder panel, which is a
different question from how a glass surface refracts, and both now reach the
chrome through one Panel.
Two comments claiming CSS could not drive blur on either shipping renderer are
gone: vello's backdrop pass landed, and `blitz-paint` converts and forwards the
filter, so the chain is whole from stylesheet to scene.
---
apps/gui/frontend/src/components/Panel.tsx | 8 +-
.../src/features/settings/SettingsTab.tsx | 171 ++++++++++++++++--
apps/gui/frontend/src/i18n/ui/en.ts | 9 +
apps/gui/frontend/src/i18n/ui/zh.ts | 8 +
apps/gui/frontend/src/lib/theme.ts | 43 ++++-
apps/gui/frontend/src/styles/theme.css | 83 +++++++++
apps/gui/frontend/src/types/index.ts | 18 ++
7 files changed, 317 insertions(+), 23 deletions(-)
diff --git a/apps/gui/frontend/src/components/Panel.tsx b/apps/gui/frontend/src/components/Panel.tsx
index 6534ef9c..027a62a3 100644
--- a/apps/gui/frontend/src/components/Panel.tsx
+++ b/apps/gui/frontend/src/components/Panel.tsx
@@ -13,12 +13,18 @@ export type PanelProps = JSX.HTMLAttributes & {
* Radius 14 on `base-100` with a `base-content/9%` hairline — the design's
* panel token. Everything that looks like a card on screen is either this or a
* row inside it.
+ *
+ * `az-glass` is what makes that surface material rather than a flat fill: it
+ * takes the twenty-five `--glass-*` tokens `@pathscale/ui` derives from three
+ * numbers, so the Appearance sliders reach every panel at once. It is listed
+ * after `az-panel` because it replaces that opaque background — a backdrop
+ * filter behind an opaque fill blurs nothing anyone can see.
*/
export function Panel(props: PanelProps): JSX.Element {
const [own, rest] = splitProps(props, ["children", "class"]);
return (
{own.children}
diff --git a/apps/gui/frontend/src/features/settings/SettingsTab.tsx b/apps/gui/frontend/src/features/settings/SettingsTab.tsx
index f1d1c983..7d6bd4ec 100644
--- a/apps/gui/frontend/src/features/settings/SettingsTab.tsx
+++ b/apps/gui/frontend/src/features/settings/SettingsTab.tsx
@@ -1,4 +1,17 @@
-import { Checkbox, Flex, Input, Select, Slider, Switch, Textarea } from "@pathscale/ui";
+import {
+ applyGlassTokens,
+ Checkbox,
+ Flex,
+ GLASS_DEFAULTS,
+ GLASS_LIMITS,
+ type GlassMode,
+ type GlassTuning,
+ Input,
+ Select,
+ Slider,
+ Switch,
+ Textarea,
+} from "@pathscale/ui";
import {
createContext,
createEffect,
@@ -44,6 +57,7 @@ import type {
StudySummary,
TableSize,
TaskManagerSettings,
+ ThemeSettings,
} from "~/types";
import { ThemePicker } from "./ThemePicker";
@@ -1029,14 +1043,20 @@ export function SettingsTab(): JSX.Element {
/>
{/*
- Three axes, and every one of them moves something. There is no
- blur slider: blur belongs to the compositor's glass view and
- CSS cannot drive it on either shipping renderer, so it would
- be a control that does nothing.
-
- All three lean on the hue ladder rather than white. Frost
- needs something light beneath it to diffuse; on a dark surface
- a white film is grey haze on the colour, not material.
+ Two families of axis, and the split is real rather than
+ historical.
+
+ These three style AgencyZero's own `.az-panel`, which is
+ opaque, and they lean on the hue ladder rather than white:
+ frost needs something light beneath it to diffuse, so on a
+ dark surface a white film is grey haze on the colour, not
+ material.
+
+ The three below them are the library's, and they style
+ anything rendered with `material="glass"`. `@pathscale/ui`
+ derives twenty-five `--glass-*` tokens from exactly those
+ numbers, so they are passed straight through rather than
+ reinterpreted here.
*/}
-
+
+
+
+ `${Math.round(value)}px`}
+ onChange={(glassBlur) => void actions.saveSettings({ theme: { glassBlur } })}
+ />
+
+
+
+ `${Math.round((value / GLASS_LIMITS.refraction.max) * 100)}%`
+ }
+ onChange={(glassRefraction) =>
+ void actions.saveSettings({ theme: { glassRefraction } })
+ }
+ />
+
+
+ `${Math.round((value / GLASS_LIMITS.depth.max) * 100)}%`}
+ onChange={(glassDepth) => void actions.saveSettings({ theme: { glassDepth } })}
+ />
+ ;
+
/**
* One appearance axis.
*
@@ -2816,6 +2886,71 @@ function GlassAxis(props: {
);
}
+/**
+ * One of the library's three glass numbers.
+ *
+ * Separate from {@link GlassAxis} because it writes a different thing: that one
+ * sets a single `--az-glass-*` property on AgencyZero's own opaque panel, and
+ * this one hands `blur`/`refraction`/`depth` to `@pathscale/ui`, which derives
+ * twenty-five `--glass-*` tokens from the three together. A preview therefore
+ * has to re-derive the whole set from the other two axes as they stand, which
+ * is what `applyGlassTokens` does when given a complete tuning.
+ *
+ * Range and default both come from the library rather than being restated here.
+ * That is the point of the exercise: glass is the library's primitive now, so
+ * an app that hardcoded `max={0.4}` would be a second place to update when the
+ * curves are retuned.
+ */
+function GlassTuningAxis(props: {
+ label: string;
+ axis: keyof GlassTuning;
+ step: number;
+ /** Undefined means "whatever the library defaults this axis to". */
+ value: number | undefined;
+ /** The theme these three axes live on, for re-deriving the whole set. */
+ theme: ThemeSettings;
+ format: (value: number) => string;
+ onChange: (value: number) => void;
+}): JSX.Element {
+ const mode = (): GlassMode =>
+ document.documentElement.dataset.colorMode === "light" ? "light" : "dark";
+ const resolved = (axis: keyof GlassTuning): number => {
+ const value = props.theme[GLASS_SETTING_KEYS[axis]];
+ return Number.isFinite(value) ? Number(value) : GLASS_DEFAULTS[mode()][axis];
+ };
+ const limits = () => GLASS_LIMITS[props.axis];
+
+ return (
+
+ );
+}
+
/** The manual snapshot button, beside Backups. */
function StoreSnapshotControl(): JSX.Element {
const { actions, isLive } = useWorkspace();
diff --git a/apps/gui/frontend/src/i18n/ui/en.ts b/apps/gui/frontend/src/i18n/ui/en.ts
index 59a0b407..e3a72458 100644
--- a/apps/gui/frontend/src/i18n/ui/en.ts
+++ b/apps/gui/frontend/src/i18n/ui/en.ts
@@ -958,6 +958,15 @@ const en = {
"strength of the hairline around a panel": "strength of the hairline around a panel",
"Panel depth": "Panel depth",
"drop shadow under a panel": "drop shadow under a panel",
+ "Glass blur": "Glass blur",
+ "how far a glass surface smears what is behind it":
+ "how far a glass surface smears what is behind it",
+ "Glass refraction": "Glass refraction",
+ "how much a glass surface asserts its own tint, border and highlight":
+ "how much a glass surface asserts its own tint, border and highlight",
+ "Glass depth": "Glass depth",
+ "how far a glass surface sits off the page: glow, sheen and shadow":
+ "how far a glass surface sits off the page: glow, sheen and shadow",
Snapshot: "Snapshot",
"a copy of the store as it stands, without closing the app":
"a copy of the store as it stands, without closing the app",
diff --git a/apps/gui/frontend/src/i18n/ui/zh.ts b/apps/gui/frontend/src/i18n/ui/zh.ts
index abd322f8..df23b2e6 100644
--- a/apps/gui/frontend/src/i18n/ui/zh.ts
+++ b/apps/gui/frontend/src/i18n/ui/zh.ts
@@ -927,6 +927,14 @@ const zh = {
"strength of the hairline around a panel": "面板细边框的强度",
"Panel depth": "面板深度",
"drop shadow under a panel": "面板下的投影",
+ "Glass blur": "玻璃模糊",
+ "how far a glass surface smears what is behind it": "玻璃表面对背后内容的模糊程度",
+ "Glass refraction": "玻璃折射",
+ "how much a glass surface asserts its own tint, border and highlight":
+ "玻璃表面自身色调、边框和高光的强度",
+ "Glass depth": "玻璃深度",
+ "how far a glass surface sits off the page: glow, sheen and shadow":
+ "玻璃表面离页面的高度:辉光、光泽和阴影",
Snapshot: "快照",
"a copy of the store as it stands, without closing the app": "在不关闭应用的情况下复制当前存储",
"Take snapshot": "创建快照",
diff --git a/apps/gui/frontend/src/lib/theme.ts b/apps/gui/frontend/src/lib/theme.ts
index de8dc3d1..5b54a1ca 100644
--- a/apps/gui/frontend/src/lib/theme.ts
+++ b/apps/gui/frontend/src/lib/theme.ts
@@ -1,3 +1,4 @@
+import { applyGlassTokens, GLASS_DEFAULTS, type GlassMode } from "@pathscale/ui";
import type { ColorValue } from "@pathscale/ui/components/color-wheel-flower";
import type { ThemeSettings } from "~/types";
@@ -225,18 +226,52 @@ export function applyTheme(
* Each default is the untouched look: no lift, the 16% hairline, no shadow.
* A property is removed rather than written at its default, so the
* stylesheet's own value applies and nothing inline needs explaining later.
- *
- * There is no blur axis. Blur belongs to the compositor's glass view, and CSS
- * `backdrop-filter` cannot drive it on either shipping renderer, so a blur
- * slider would move nothing.
*/
writeGlassAxis("lift", theme.glassLift);
writeGlassAxis("border", theme.glassBorder);
writeGlassAxis("shadow", theme.glassShadow);
+ writeGlassTuning(theme, root);
return windowChrome(accent, theme);
}
+/**
+ * The library's glass, from the three numbers it derives everything else from.
+ *
+ * `@pathscale/ui` styles `material="glass"` off a family of `--glass-*` custom
+ * properties and ships `applyGlassTokens` to write the twenty-five derived ones
+ * from `blur`, `refraction` and `depth`. Calling it here rather than keeping a
+ * local copy of the curves means the app and the library cannot disagree about
+ * what glass looks like, and a library that retunes them retunes this too.
+ *
+ * An axis the settings leave undefined falls to `GLASS_DEFAULTS` for the
+ * current colour mode, so this is always a complete set: three of the tokens
+ * are read by the component CSS without a fallback, and a partial set gives a
+ * card with no background rather than a plainer one.
+ *
+ * Blur used to be excluded here, on the grounds that neither shipping renderer
+ * could carry `backdrop-filter`. That stopped being true: vello now has a real
+ * backdrop pass (`record_backdrop`), so the blur axis reaches the screen.
+ */
+function writeGlassTuning(theme: ThemeSettings, root: HTMLElement): void {
+ // Read from the root rather than importing the prefs store: this module is a
+ // pure function of the theme it is handed, and `stores/prefs` already writes
+ // the mode here on every change.
+ const mode: GlassMode = root.dataset.colorMode === "light" ? "light" : "dark";
+ const defaults = GLASS_DEFAULTS[mode];
+ applyGlassTokens(
+ {
+ blur: Number.isFinite(theme.glassBlur) ? Number(theme.glassBlur) : defaults.blur,
+ refraction: Number.isFinite(theme.glassRefraction)
+ ? Number(theme.glassRefraction)
+ : defaults.refraction,
+ depth: Number.isFinite(theme.glassDepth) ? Number(theme.glassDepth) : defaults.depth,
+ },
+ mode,
+ root,
+ );
+}
+
/**
* The three glass axes, and their defaults.
*
diff --git a/apps/gui/frontend/src/styles/theme.css b/apps/gui/frontend/src/styles/theme.css
index c858539f..bf655a07 100644
--- a/apps/gui/frontend/src/styles/theme.css
+++ b/apps/gui/frontend/src/styles/theme.css
@@ -538,6 +538,89 @@
box-shadow: 0 8px 32px rgb(0 0 0 / var(--az-glass-shadow, 0));
}
+ /*
+ * Real glass, from the library's tokens.
+ *
+ * `@pathscale/ui` derives twenty-five `--glass-*` properties from three
+ * numbers, and every one below is read with the fallback the library's own
+ * component CSS uses. That is load-bearing rather than tidy: an undefined
+ * custom property does not fall back to its initial value, CSS drops the
+ * whole declaration, so a bare read here would give a panel with no
+ * background at all rather than a plainer one.
+ *
+ * It replaces `.az-panel`'s opaque fill rather than sitting over it, because
+ * a backdrop filter behind an opaque background blurs nothing. The hue ladder
+ * still supplies the tint: `az-badge` is a lighter rung of the same hue, and
+ * a white film on a dark surface reads as grey haze rather than as material.
+ *
+ * Blur reaches the screen now. The vello backend that ships gained a real
+ * backdrop pass, so `backdrop-filter` is no longer a property the renderer
+ * accepts and discards.
+ */
+ .az-glass {
+ background-color: color-mix(
+ in oklab,
+ var(--color-az-badge),
+ transparent calc(100% - var(--glass-app-surface-opacity, 38%))
+ );
+ outline-color: color-mix(
+ in oklab,
+ var(--color-primary),
+ transparent calc(100% - var(--glass-app-border-opacity, 30%))
+ );
+ box-shadow:
+ var(--glass-shadow-depth, 0 8px 32px rgb(0 0 0 / 10%)),
+ inset 0 1px 0
+ color-mix(
+ in oklab,
+ var(--color-az-badge),
+ transparent calc(100% - var(--glass-highlight-opacity, 20%))
+ );
+ backdrop-filter: blur(var(--glass-blur, 9px)) saturate(var(--glass-saturation, 1.2))
+ brightness(var(--glass-brightness, 1));
+ -webkit-backdrop-filter: blur(var(--glass-blur, 9px)) saturate(var(--glass-saturation, 1.2))
+ brightness(var(--glass-brightness, 1));
+ }
+
+ /*
+ * Nested glass is one pane, not two.
+ *
+ * A blurred surface inside another blurred surface reads the already-blurred
+ * pixels and smears them again, which is muddy rather than deeper. The same
+ * rule the library applies to its own nested surfaces.
+ */
+ .az-glass .az-glass {
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+
+ /*
+ * No backdrop support, or a reader who asked for less transparency: keep the
+ * surface legible by making it more opaque rather than leaving it a tint over
+ * whatever is behind it.
+ */
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
+ .az-glass {
+ background-color: color-mix(
+ in oklab,
+ var(--color-az-badge),
+ transparent calc(100% - var(--glass-fallback-background-opacity, 78%))
+ );
+ }
+ }
+
+ @media (prefers-reduced-transparency: reduce) {
+ .az-glass {
+ background-color: color-mix(
+ in oklab,
+ var(--color-az-badge),
+ transparent calc(100% - var(--glass-fallback-background-opacity, 78%))
+ );
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ }
+ }
+
@media (prefers-reduced-motion: reduce) {
/* biome-ignore lint/style/noDescendingSpecificity: this sets animation and
transition only, and shares no property with the selection rules above
diff --git a/apps/gui/frontend/src/types/index.ts b/apps/gui/frontend/src/types/index.ts
index f0c74c38..41fe6f8f 100644
--- a/apps/gui/frontend/src/types/index.ts
+++ b/apps/gui/frontend/src/types/index.ts
@@ -700,6 +700,24 @@ export interface ThemeSettings {
glassBorder?: number;
/** Panel drop shadow, 0 to 1. 0 is none. */
glassShadow?: number;
+ /*
+ * The library's three glass numbers, which derive twenty-five `--glass-*`
+ * tokens between them. `@pathscale/ui`'s `GLASS_LIMITS` owns the ranges and
+ * `GLASS_DEFAULTS` the values, so nothing here restates either: an axis left
+ * undefined is the library's default for the current colour mode.
+ *
+ * These sit beside the three above rather than replacing them because they
+ * answer different questions. `glassLift`/`glassBorder`/`glassShadow` style
+ * AgencyZero's own opaque `.az-panel` on the hue ladder; these style anything
+ * the library renders with `material="glass"`, which is what the app is
+ * moving to.
+ */
+ /** Backdrop blur radius in pixels, 0 to 50. */
+ glassBlur?: number;
+ /** How much a glass surface asserts itself: tint, border, highlight. 0 to 0.4. */
+ glassRefraction?: number;
+ /** How far a glass surface sits off the page: glow, sheen, shadow. 0 to 30. */
+ glassDepth?: number;
/**
* How much of the surface colour is mixed into every surface, as a percentage.
* Ignored while `surface` is empty.
From 0733d262c3a6d3586f478590be89f41d803a33a8 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 14:12:17 +0700
Subject: [PATCH 09/11] docs(glass): record that in-app glass shipped, and that
window glass did not
The document planned both halves and its status section only ever described the
window one. In-app glass is now live through the library's tokens and needed no
window transparency at all, which is worth stating next to the two switches
that are still shut, so the next reader does not open them expecting to be
turning glass on for the first time.
---
docs/glass-support.md | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/docs/glass-support.md b/docs/glass-support.md
index 0ce519be..b59ace9f 100644
--- a/docs/glass-support.md
+++ b/docs/glass-support.md
@@ -77,6 +77,25 @@ The second can ship without the first and is where the existing groundwork is.
handful of properties and neither `outline` nor `box-shadow` is among them.
See `tests/blitz-tests/tests/panel_axes.rs`.
+### Landed 2026-08-16: in-app glass, from the library's three numbers
+
+In-app glass ships. `@pathscale/ui` 2.5 carries `styles/glass.ts`, which derives
+twenty-five `--glass-*` tokens from `blur`, `refraction` and `depth` — the
+curves lifted from the same consulting.parcle.ai tuning surface, against this
+same token vocabulary. `applyTheme` calls its `applyGlassTokens`, Settings
+exposes the three axes with `GLASS_LIMITS`/`GLASS_DEFAULTS` supplying ranges and
+defaults, and `.az-glass` on `components/Panel.tsx` is what reads the tokens, so
+every panel in the app is one glass surface.
+
+That is the **in-app** half of the split this document opens with, and it needed
+no window transparency: it is CSS `backdrop-filter`, blurring what is behind a
+panel inside an opaque window.
+
+**Window glass is still off, and the section below is still why.** The two
+switches named there are unchanged, because an `NSGlassEffectView` over an
+opaque window still flattens the app under one colour. What changed is that
+in-app glass no longer waits on them.
+
### Superseded 2026-08-16: the blur landed, the switches did not
The table below is kept because its reasoning is still useful, but its verdict
From 9ebe4ba5023b80c5e85baf4959e3e02fae2a675f Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 14:14:47 +0700
Subject: [PATCH 10/11] fix(glass): read the token names 2.5 actually produces
`.az-glass` asked for `--glass-app-surface-opacity` and
`--glass-app-border-opacity`. Those belong to the older parcle vocabulary; the
library's own producer emits twenty-four tokens and neither is among them, so
both reads fell to their fallbacks and the refraction axis moved nothing on a
panel's fill or edge.
The names that carry the same two values are `--glass-background-opacity` and
`--glass-border-opacity`, which is what `Card.css` reads for its own glass
material. Checked the whole block against the producer's output rather than the
two that were wrong: all eight tokens now resolve.
---
apps/gui/frontend/src/styles/theme.css | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/gui/frontend/src/styles/theme.css b/apps/gui/frontend/src/styles/theme.css
index bf655a07..7a5f646d 100644
--- a/apps/gui/frontend/src/styles/theme.css
+++ b/apps/gui/frontend/src/styles/theme.css
@@ -561,12 +561,12 @@
background-color: color-mix(
in oklab,
var(--color-az-badge),
- transparent calc(100% - var(--glass-app-surface-opacity, 38%))
+ transparent calc(100% - var(--glass-background-opacity, 55%))
);
outline-color: color-mix(
in oklab,
var(--color-primary),
- transparent calc(100% - var(--glass-app-border-opacity, 30%))
+ transparent calc(100% - var(--glass-border-opacity, 12%))
);
box-shadow:
var(--glass-shadow-depth, 0 8px 32px rgb(0 0 0 / 10%)),
From 014d850d7e9d126375aa277b0ef86f709f1198b8 Mon Sep 17 00:00:00 2001
From: meh
Date: Sun, 16 Aug 2026 16:24:17 +0700
Subject: [PATCH 11/11] fix: repin the engine for the grid track sizing hang,
and 0.8.4
A table in a transcript could wedge the app permanently. ps-taffy's
`distribute_space_up_to_limits` kept a track counted as growable while
`property + increase < limit`, but sized the iteration from
`limit - property - increase`, and f32 rounding lets the first be true while
the second is exactly zero. The increase came out as zero, the guard below it
rejected every track, and nothing changed, so the next iteration recomputed the
same values forever.
Layout runs on the main thread, so this presented as a beachball rather than a
slow frame, and it survived SIGTERM: the handler drains the tables from a
signal thread, but the exit that follows goes through the event loop the loop
was holding. Reopening the app restored the same transcript and wedged again.
The composer reads `scrollHeight` on every keystroke, which resolves the whole
document, so any table on screen made a single keypress enough to trigger it.
Chain: ps-taffy 4355471d, ps-blitz b82e649d, tauri-runtime-blitz 9edbec23.
All the ps-blitz pins move together, since cargo treats one git source at two
revisions as two crates and the shared traits then exist twice.
apps/blitz-preview keeps the old pin. It is a separate workspace whose lock
cannot be regenerated: it patches blitz-dom to ~/code/blitz-rust, which does
not exist, so its pins were already unbuildable before this change.
---
Cargo.lock | 66 +++++++++++++++++------------------
Cargo.toml | 2 +-
apps/gui/Cargo.toml | 6 ++--
crates/blitz-bench/Cargo.toml | 2 +-
4 files changed, 38 insertions(+), 38 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2020e9e6..b66d1dc9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,7 +72,7 @@ dependencies = [
[[package]]
name = "agency-tools"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"derive_more 2.1.1",
"dirs",
@@ -364,11 +364,11 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "az-core"
-version = "0.8.3"
+version = "0.8.4"
[[package]]
name = "az-gui"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"agency-proxy-client",
"agency-proxy-protocol",
@@ -407,7 +407,7 @@ dependencies = [
[[package]]
name = "az-mcp-proxy"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"az-core",
]
@@ -498,7 +498,7 @@ dependencies = [
[[package]]
name = "blitz-bench"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"blitz-control-protocol",
"endpoint-libs",
@@ -511,7 +511,7 @@ dependencies = [
[[package]]
name = "blitz-control-protocol"
version = "0.1.0"
-source = "git+https://github.com/pathscale/tauri-runtime-blitz.git?rev=070ab3a2fd460d7c9b2f3f341e7d6c50af669682#070ab3a2fd460d7c9b2f3f341e7d6c50af669682"
+source = "git+https://github.com/pathscale/tauri-runtime-blitz.git?rev=9edbec23ce4ca9bcb052a364577fd9a736fd81a3#9edbec23ce4ca9bcb052a364577fd9a736fd81a3"
dependencies = [
"endpoint-libs",
"serde",
@@ -521,7 +521,7 @@ dependencies = [
[[package]]
name = "blitz-html"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"html5ever 0.39.0",
"ps-blitz-dom",
@@ -532,7 +532,7 @@ dependencies = [
[[package]]
name = "blitz-paint"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"color",
"euclid",
@@ -1056,7 +1056,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
- "unicode-width 0.1.14",
+ "unicode-width 0.2.2",
]
[[package]]
@@ -1521,7 +1521,7 @@ checksum = "da220af51a1a335e9a930beaaef53d261e41ea9eecfb3d973a3ddae1a7284b9c"
[[package]]
name = "debug_timer"
version = "0.1.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
[[package]]
name = "deranged"
@@ -1625,7 +1625,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1896,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2594,7 +2594,7 @@ dependencies = [
"log",
"presser",
"thiserror 2.0.19",
- "windows 0.61.3",
+ "windows 0.62.2",
]
[[package]]
@@ -2926,7 +2926,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.61.2",
+ "windows-core 0.62.2",
]
[[package]]
@@ -3231,7 +3231,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3641,7 +3641,7 @@ dependencies = [
"png 0.18.1",
"serde",
"thiserror 2.0.19",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4608,7 +4608,7 @@ dependencies = [
[[package]]
name = "ps-blitz-debug-control"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"getrandom 0.4.3",
"serde",
@@ -4618,7 +4618,7 @@ dependencies = [
[[package]]
name = "ps-blitz-dom"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"accesskit",
"app_units",
@@ -4627,7 +4627,7 @@ dependencies = [
"color",
"cssparser 0.37.0",
"cursor-icon",
- "debug_timer 0.1.3 (git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874)",
+ "debug_timer 0.1.3 (git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557)",
"euclid",
"html-escape",
"image",
@@ -4663,7 +4663,7 @@ dependencies = [
[[package]]
name = "ps-blitz-script"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"base64 0.22.1",
"blitz-html",
@@ -4690,7 +4690,7 @@ dependencies = [
[[package]]
name = "ps-blitz-shell"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"android-activity",
"arboard",
@@ -4711,7 +4711,7 @@ dependencies = [
[[package]]
name = "ps-blitz-traits"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"atomic_refcell",
"bitflags 2.13.1",
@@ -4727,7 +4727,7 @@ dependencies = [
[[package]]
name = "ps-taffy"
version = "0.13.0"
-source = "git+https://github.com/pathscale/ps-taffy.git?rev=4f86730bab59a1b1910fb5adb29a5dec6415d5f7#4f86730bab59a1b1910fb5adb29a5dec6415d5f7"
+source = "git+https://github.com/pathscale/ps-taffy.git?rev=4355471d36f1939109d36197bfb580a931231738#4355471d36f1939109d36197bfb580a931231738"
dependencies = [
"arrayvec",
"serde",
@@ -5296,7 +5296,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5352,7 +5352,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5520,7 +5520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5928,7 +5928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -6181,7 +6181,7 @@ dependencies = [
[[package]]
name = "stylo_taffy"
version = "0.3.0-beta.3"
-source = "git+https://github.com/pathscale/ps-blitz.git?rev=a81bea599c2f67a9a1568e0f8911435c59af8874#a81bea599c2f67a9a1568e0f8911435c59af8874"
+source = "git+https://github.com/pathscale/ps-blitz.git?rev=b82e649d4b89757897a74a23274cedd2c096e557#b82e649d4b89757897a74a23274cedd2c096e557"
dependencies = [
"ps-taffy",
"stylo",
@@ -6616,7 +6616,7 @@ dependencies = [
[[package]]
name = "tauri-runtime-blitz"
version = "0.1.0"
-source = "git+https://github.com/pathscale/tauri-runtime-blitz.git?rev=070ab3a2fd460d7c9b2f3f341e7d6c50af669682#070ab3a2fd460d7c9b2f3f341e7d6c50af669682"
+source = "git+https://github.com/pathscale/tauri-runtime-blitz.git?rev=9edbec23ce4ca9bcb052a364577fd9a736fd81a3#9edbec23ce4ca9bcb052a364577fd9a736fd81a3"
dependencies = [
"blitz-control-protocol",
"endpoint-libs",
@@ -6699,7 +6699,7 @@ dependencies = [
"serde_with",
"swift-rs",
"thiserror 2.0.19",
- "toml 0.9.12+spec-1.1.0",
+ "toml 1.1.3+spec-1.1.0",
"url",
"urlpattern",
"uuid",
@@ -6727,7 +6727,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8091,7 +8091,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.60.2",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8978,7 +8978,7 @@ dependencies = [
[[package]]
name = "wt-migrate"
-version = "0.8.3"
+version = "0.8.4"
dependencies = [
"chrono",
"derive_more 2.1.1",
diff --git a/Cargo.toml b/Cargo.toml
index f2e51413..7d9345a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ members = [
]
[workspace.package]
-version = "0.8.3"
+version = "0.8.4"
edition = "2024"
publish = false
diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml
index 12c0f433..cfcf6c78 100644
--- a/apps/gui/Cargo.toml
+++ b/apps/gui/Cargo.toml
@@ -72,9 +72,9 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "serd
tokio = { version = "1", features = ["process", "rt-multi-thread", "time"] }
# Default features are off; see `webview-runtime` for why.
tauri = { version = "2", default-features = false, features = ["compression"] }
-tauri-runtime-blitz = { git = "https://github.com/pathscale/tauri-runtime-blitz.git", rev = "070ab3a2fd460d7c9b2f3f341e7d6c50af669682", optional = true }
-blitz-dom = { package = "ps-blitz-dom", git = "https://github.com/pathscale/ps-blitz.git", rev = "a81bea599c2f67a9a1568e0f8911435c59af8874", features = ["system-fonts", "parallel-construct"], optional = true }
-blitz-script = { package = "ps-blitz-script", git = "https://github.com/pathscale/ps-blitz.git", rev = "a81bea599c2f67a9a1568e0f8911435c59af8874", features = ["system-fonts"], optional = true }
+tauri-runtime-blitz = { git = "https://github.com/pathscale/tauri-runtime-blitz.git", rev = "9edbec23ce4ca9bcb052a364577fd9a736fd81a3", optional = true }
+blitz-dom = { package = "ps-blitz-dom", git = "https://github.com/pathscale/ps-blitz.git", rev = "b82e649d4b89757897a74a23274cedd2c096e557", features = ["system-fonts", "parallel-construct"], optional = true }
+blitz-script = { package = "ps-blitz-script", git = "https://github.com/pathscale/ps-blitz.git", rev = "b82e649d4b89757897a74a23274cedd2c096e557", features = ["system-fonts"], optional = true }
brotli = { version = "8.0.4", default-features = false, features = ["std"], optional = true }
url = { version = "2.5.8", optional = true }
tauri-plugin-updater = "2"
diff --git a/crates/blitz-bench/Cargo.toml b/crates/blitz-bench/Cargo.toml
index 6fadd784..24ac594b 100644
--- a/crates/blitz-bench/Cargo.toml
+++ b/crates/blitz-bench/Cargo.toml
@@ -11,7 +11,7 @@ publish.workspace = true
# `tauri-runtime-blitz` for the same types would build a browser engine to send
# a wheel event. `cargo tree -p blitz-bench` is the check.
[dependencies]
-blitz-control-protocol = { git = "https://github.com/pathscale/tauri-runtime-blitz.git", rev = "070ab3a2fd460d7c9b2f3f341e7d6c50af669682" }
+blitz-control-protocol = { git = "https://github.com/pathscale/tauri-runtime-blitz.git", rev = "9edbec23ce4ca9bcb052a364577fd9a736fd81a3" }
# The same framing the server uses, from the same crate, for the same reason as
# the protocol types: a hand-rolled length-prefix reader is how the Python
# client hung on its first malformed frame.