From 1034ab6cb8405a9a17d190181dd71aad88106cad Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 09:36:49 +0700 Subject: [PATCH 01/23] ci: bound the jobs, and stop rebuilding the sidecars every run The Rust job on master ran past twenty minutes with no end in sight. It was not wedged on anything this repository compiles: clippy had been running since 02:33 where the same step takes 65 seconds warm, because a cache entry never existed for it. Every cache in the repository is scoped to `refs/pull/211/merge` and keyed `Darwin-arm64`, which is the macOS panel job. Caches are branch scoped and a branch can only read its own and its base's, so nothing on master ever had one to read, and now that #211 is merged those entries are unreachable anyway. Neither job declared `timeout-minutes`, so the ceiling was GitHub's default of six hours. That is the part that turns a slow step into an invisible one: the run does not fail, it just never finishes, and the queue backs up behind it. Both jobs now have a bound sized to their warm time with room for a cold graph. The sidecars are the other two and a half minutes. Both build outside the workspace target directory, which is what `rust-cache` keys on, so neither was ever cached, and both are the least likely things in the tree to change: the proxy is a fixed-version crates.io install, and the reader is pinned to the v2-era WorkTable it must never move off. They are now cached as the produced binaries, keyed on the manifests, scripts, reader sources and GUI schema that decide them, since the reader's build script derives its schema copy from that. `runner.os` is in the key, so a Linux runner cannot pick up the host-target-suffixed macOS binaries sitting in a developer's checkout. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7c4456e..d1587e99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: # cheap because it runs on every push. The only macOS in this repository # is the release bundle, which cannot be built anywhere else. runs-on: ubicloud-standard-2 + # This job is 1m20s warm and has no network wait that can stall. Without a + # ceiling a hang runs to GitHub's six-hour default, which is how a wedged + # step reads as an afternoon of queued CI rather than a failure. + timeout-minutes: 6 defaults: run: working-directory: apps/gui/frontend @@ -49,6 +53,12 @@ jobs: rust: name: Rust runs-on: ubicloud-standard-4 + # Warm, this job is about five minutes end to end, and cold it is closer to + # fifteen because the whole dependency graph compiles. Twenty leaves room + # for a cold cache after a dependency bump without leaving a genuinely + # wedged step to run for six hours, which is the default and is how a hang + # becomes invisible: the run just never finishes. + timeout-minutes: 20 env: # CI needs diagnostics, not debugger symbol tables. This substantially # shrinks compile and link work without changing which tests execute. @@ -93,10 +103,25 @@ jobs: bun install --no-save bun run build + # Both sidecars build outside the workspace target directory, which is + # what `rust-cache` keys on, so neither was cached and together they cost + # about two and a half minutes of every run. They are also the two things + # least likely to change: the proxy is a fixed-version crates.io install, + # and the reader is pinned to a v2-era WorkTable it must never move off. + # Cache the produced binaries against the inputs that decide them. + - name: Cache the built sidecars + id: sidecars + uses: actions/cache@v4 + with: + path: apps/gui/binaries + key: sidecars-${{ runner.os }}-${{ hashFiles('Cargo.toml', 'crates/wt-migrate/v2-reader/Cargo.toml', 'crates/wt-migrate/v2-reader/**/*.rs', 'apps/gui/src/db/schema/*.rs', 'scripts/stage-agency-proxy-sidecar.sh', 'scripts/stage-wt-v2-reader-sidecar.sh') }} + - name: Build the AgencyProxy sidecar + if: steps.sidecars.outputs.cache-hit != 'true' run: scripts/stage-agency-proxy-sidecar.sh - name: Build the WorkTable v2 migration reader + if: steps.sidecars.outputs.cache-hit != 'true' run: scripts/stage-wt-v2-reader-sidecar.sh # Named per workspace member rather than `--all`, which reaches into any From 3b69129ceb8aa5e4640d69995cb2fafbbc999cdb Mon Sep 17 00:00:00 2001 From: meh Date: Fri, 18 Sep 2026 09:40:44 +0700 Subject: [PATCH 02/23] ci: clippy runs when asked for, not on every push It was the longest step in the Rust job: 65 seconds warm, minutes cold. `--all-targets` type-checks the test and bench targets on top of the lib and bins, which is close to double the compile work, and `cargo test` on the next line then does nearly all of it again. A lint is worth most while the code is still open, which is the machine doing the writing, not one reporting six minutes later. It stays wired up here behind `workflow_dispatch` with a `clippy` input, so the machine's answer is one manual run away when someone wants it. `inputs.clippy` is undefined on push and pull_request, so the step is skipped there without needing a second condition. The exact local command is in the comment above the step, because it was documented nowhere: not in a gates file, not in CLAUDE instructions, and the only mention of clippy in `docs/` is one review from July. --- .github/workflows/ci.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1587e99..f86b8c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,12 @@ on: push: branches: [master] pull_request: + workflow_dispatch: + inputs: + clippy: + description: "Also run cargo clippy (slow: it type-checks every test target too)" + type: boolean + default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -135,8 +141,22 @@ jobs: # workspace-wide gate. Its compile is covered by the staging step; this is # the formatting half of the same blind spot. - run: cargo fmt --check --manifest-path crates/wt-migrate/v2-reader/Cargo.toml --all + # Clippy runs on request, not on every push. It was the longest step in + # this job, 65 seconds warm and minutes cold, because `--all-targets` + # type-checks the test and bench targets on top of the lib and bins: + # close to double the compile work, nearly all of it repeated by + # `cargo test` below. A lint wants to be read and fixed while the code is + # open, so it belongs on the machine doing the writing: + # + # cargo clippy --workspace --all-targets --no-default-features \ + # --features experimental,webview-runtime -- -D warnings + # + # To get the machine's answer anyway, run this workflow from the Actions + # tab with `clippy: true`. + # # Blitz is currently a macOS preview runtime. Enabling every feature on # this Linux runner asks it to implement Tauri's GTK-only runtime traits, # which is separate portability work rather than Linux CI for AgencyZero. - - run: cargo clippy --workspace --all-targets --no-default-features --features experimental,webview-runtime -- -D warnings + - if: inputs.clippy + run: cargo clippy --workspace --all-targets --no-default-features --features experimental,webview-runtime -- -D warnings - run: cargo test --workspace --no-default-features --features experimental,webview-runtime From b7f61caf9ba9fa62b31e9bcd9f99d8abb33ff125 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 08:05:41 +0700 Subject: [PATCH 03/23] Keep the space a wrapped line puts before inline code A chat message read "and`WireMessage::Text`now takes`Utf8Bytes`": the spaces around every inline code span were gone. A paragraph is delimited by a blank line, so a single newline inside one is a wrap in the source rather than a break the author asked for. Between two plain words HTML collapses that newline to a space, which is why this went unnoticed for so long. Next to an inline element it does not: a text node ending in "and\n" followed by a element has its trailing whitespace dropped at the element boundary, and the two run together. Normalising soft-wrap newlines to spaces before the inline split keeps the space inside the text node, where the boundary cannot eat it. In its own module rather than beside the renderer, so the test reaches it without importing MessageBody, which pulls in the component library and a motion entry point vitest cannot transform. --- .../src/features/project/MessageBody.tsx | 4 ++- .../features/project/inlineSpacing.test.ts | 30 +++++++++++++++++++ .../frontend/src/features/project/softWrap.ts | 19 ++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 apps/gui/frontend/src/features/project/inlineSpacing.test.ts create mode 100644 apps/gui/frontend/src/features/project/softWrap.ts diff --git a/apps/gui/frontend/src/features/project/MessageBody.tsx b/apps/gui/frontend/src/features/project/MessageBody.tsx index 90391467..1d8fc620 100644 --- a/apps/gui/frontend/src/features/project/MessageBody.tsx +++ b/apps/gui/frontend/src/features/project/MessageBody.tsx @@ -1,5 +1,6 @@ import type { JSX } from "@solidjs/web"; import { createMemo, createSignal, For, Show } from "solid-js"; +import { softWrapToSpaces } from "./softWrap"; import { Button } from "~/components/Button"; import { Icon } from "~/components/Icon"; import { isPromptSyntaxDirectiveLine } from "~/features/project/promptSyntax"; @@ -670,7 +671,8 @@ function renderItemReferences(text: string, id: string): JSX.Element { ); } -function renderInline(text: string, id: string): JSX.Element[] { +function renderInline(rawText: string, id: string): JSX.Element[] { + const text = softWrapToSpaces(rawText); // One text node is the correct rendered structure for plain prose. The old // path still built two mapped arrays and nested fragments for it, multiplying // Solid/Blitz clone work across every visible paragraph on every tab mount. diff --git a/apps/gui/frontend/src/features/project/inlineSpacing.test.ts b/apps/gui/frontend/src/features/project/inlineSpacing.test.ts new file mode 100644 index 00000000..5aa4f85e --- /dev/null +++ b/apps/gui/frontend/src/features/project/inlineSpacing.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { softWrapToSpaces } from "./softWrap"; + +/** + * A paragraph is delimited by a blank line, so a single newline inside one is a + * wrap in the source rather than a break the author asked for. + * + * Between two plain words HTML collapses that newline to a space, which is why + * this went unnoticed. Next to an inline element it does not: a text node + * ending in `"and\n"` followed by `` has its trailing whitespace dropped + * at the element boundary, and the sentence renders as "and`Foo`now". + */ +describe("softWrapToSpaces", () => { + it("keeps the space before an inline code span that a wrap would eat", () => { + const wrapped = "Two changes: framing, and\n`WireMessage::Text` now takes\n`Utf8Bytes`."; + expect(softWrapToSpaces(wrapped)).toBe( + "Two changes: framing, and `WireMessage::Text` now takes `Utf8Bytes`.", + ); + }); + + it("leaves a paragraph that never wrapped alone", () => { + const flat = "Needs the `compat` feature on tokio-util."; + expect(softWrapToSpaces(flat)).toBe(flat); + }); + + it("does not collapse the spaces a line already had", () => { + expect(softWrapToSpaces("a\nb c")).toBe("a b c"); + }); +}); diff --git a/apps/gui/frontend/src/features/project/softWrap.ts b/apps/gui/frontend/src/features/project/softWrap.ts new file mode 100644 index 00000000..a52989a9 --- /dev/null +++ b/apps/gui/frontend/src/features/project/softWrap.ts @@ -0,0 +1,19 @@ +/** + * Soft-wrap newlines are spaces. + * + * A paragraph is separated by a blank line, so any single newline inside one is + * a wrap in the source rather than a break the author asked for. HTML collapses + * such a newline to a space between two words, which is why this looked correct + * until an inline element sat next to one: a text node ending in `"and\n"` + * followed by a `` element has its trailing whitespace dropped at the + * element boundary, and the sentence renders as "and`WireMessage::Text`now". + * + * Normalising before the inline split keeps the space inside the text node, + * where the boundary cannot eat it. + * + * Its own module so a test can reach it without importing the renderer, which + * pulls in the component library and its motion entry point. + */ +export function softWrapToSpaces(text: string): string { + return text.replace(/\n/g, " "); +} From 8109cadbc78d970b70ecaee6168ab0dd973a77a3 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 10:55:14 +0700 Subject: [PATCH 04/23] Answer the update check locally in a QA profile check_for_update went to the CDN with retry backoff even in a disposable QA profile. The round trip took 1 to 2s (2156ms in the failing run) against the 2s window of settings-update-check-completes, so that check's verdict was network latency. A QA process must also never be offered a published build to install over the checkout it is testing. With AZ_QA_WORKSPACE_ROOT set the command answers "no update" without the network. The check now completes in 77ms and the outcome suite is 312 of 312. --- apps/gui/src/update.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/gui/src/update.rs b/apps/gui/src/update.rs index 9fabadd7..558b8096 100644 --- a/apps/gui/src/update.rs +++ b/apps/gui/src/update.rs @@ -31,6 +31,13 @@ pub(crate) struct AvailableUpdate { /// the wrong thing to tell someone whose update check never reached the CDN. #[tauri::command] pub(crate) async fn check_for_update(app: AppHandle) -> Result, String> { + // A disposable QA profile answers without the CDN. Its checks measure the + // UI's round trip, and a real one took 1 to 2s against a 2s outcome window, + // so the verdict was network latency. It must also never be offered a + // published build to install over the checkout it is testing. + if std::env::var_os("AZ_QA_WORKSPACE_ROOT").is_some_and(|root| !root.is_empty()) { + return Ok(None); + } let updater = app.updater().map_err(|e| e.to_string())?; let found = (|| updater.check()) .retry(crate::retry::interactive_backoff()) From e6ba12dc0059a36c0f7b35051b471dd572f2745d Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 10:55:14 +0700 Subject: [PATCH 05/23] Attach the sweep to its own app by pid button-sweep.sh waited for target/blitz-control.json, which nothing writes any more, so every run ended in "ps-qa could not attach". The control server advertises -.json in a directory under $TMPDIR; the sweep now takes the one named for the pid it launched. The directory is matched by wildcard because its name is due to lose "tauri". --- scripts/button-sweep.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/button-sweep.sh b/scripts/button-sweep.sh index 8a2c6ba1..99b7891f 100755 --- a/scripts/button-sweep.sh +++ b/scripts/button-sweep.sh @@ -23,7 +23,12 @@ set -eu cd "$(dirname "$0")/.." readonly ROOT="$PWD" readonly LIVE=/tmp/qa-profile-db -readonly DESCRIPTOR="$ROOT/target/blitz-control.json" +# The control server advertises itself as `-.json` in a +# directory under $TMPDIR. Nothing writes `target/blitz-control.json` any more, +# and waiting for it made every run fail to attach. The directory is matched by +# wildcard because its name is due to lose `tauri`; the pid of the child +# launched below is what keeps this from attaching to another instance. +readonly DESCRIPTOR_ROOT="${TMPDIR:-/tmp}" surface=${1:-} if [ "$#" -gt 1 ]; then @@ -99,8 +104,12 @@ cd "$ROOT" attached=0 attach_attempt=0 +DESCRIPTOR= while [ "$attach_attempt" -lt 40 ]; do - if "$qa" nodes --descriptor "$DESCRIPTOR" >/dev/null 2>&1; then + for candidate in "${DESCRIPTOR_ROOT%/}"/*/"$APP"-*.json; do + [ -e "$candidate" ] && DESCRIPTOR=$candidate + done + if [ -n "$DESCRIPTOR" ] && "$qa" nodes --descriptor "$DESCRIPTOR" >/dev/null 2>&1; then attached=1 break fi From cd3d3ac130619314e013c70cf2cea125a357eef8 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 10:55:14 +0700 Subject: [PATCH 06/23] Clear a profile lock whose owner has exited az-gui leaves /tmp/qa-profile-db.lock behind even after a clean SIGTERM drain, so the sweep refused to start on every second run. A lock whose pid is no longer alive is stale and is removed; one held by a live process still stops the sweep. --- scripts/button-sweep.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/button-sweep.sh b/scripts/button-sweep.sh index 99b7891f..79344277 100755 --- a/scripts/button-sweep.sh +++ b/scripts/button-sweep.sh @@ -39,9 +39,18 @@ fi # Never kill by executable name: another build or the owner's stable instance # may use the same name. A pre-existing lock means this disposable profile is # already owned, so stop before deleting anything under that process. +# +# A lock whose owner has exited is stale, not owned: az-gui leaves it behind +# even after a clean SIGTERM drain, so refusing on existence alone blocked every +# second run. if [ -e "$LIVE.lock" ]; then - echo "$LIVE.lock already exists; stop its exact owner before running the sweep" >&2 - exit 1 + owner=$(sed -nE 's/^pid=([0-9]+).*/\1/p' "$LIVE.lock") + if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then + rm -f "$LIVE.lock" + else + echo "$LIVE.lock is held by live pid ${owner:-unknown}; stop that exact process first" >&2 + exit 1 + fi fi # From the committed archive, not from whatever is left in /tmp. The sweep used From bdfe3f0da5ef6e2197b25ac77a21c655ab565120 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 10:55:14 +0700 Subject: [PATCH 07/23] release: 0.8.65 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6e230c6b..1b3b20af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ exclude = ["crates/wt-migrate/v2-reader"] [workspace.package] -version = "0.8.64" +version = "0.8.65" edition = "2024" publish = false From 43ed386bf645c2e4162e521064eff2e3d5025de1 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:20:30 +0700 Subject: [PATCH 08/23] Sort the softWrap import The chat-spacing change added the import out of order, and bun run lint fails the Frontend job on it. --- apps/gui/frontend/src/features/project/MessageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gui/frontend/src/features/project/MessageBody.tsx b/apps/gui/frontend/src/features/project/MessageBody.tsx index 1d8fc620..91ef9c82 100644 --- a/apps/gui/frontend/src/features/project/MessageBody.tsx +++ b/apps/gui/frontend/src/features/project/MessageBody.tsx @@ -1,12 +1,12 @@ import type { JSX } from "@solidjs/web"; import { createMemo, createSignal, For, Show } from "solid-js"; -import { softWrapToSpaces } from "./softWrap"; import { Button } from "~/components/Button"; import { Icon } from "~/components/Icon"; import { isPromptSyntaxDirectiveLine } from "~/features/project/promptSyntax"; import { isItemId, itemReferenceLabel, revealItemReference } from "~/lib/itemReference"; import { describeError, log } from "~/lib/log"; import { tx } from "~/stores/i18n"; +import { softWrapToSpaces } from "./softWrap"; /** * Put text on the clipboard, by whichever route works here. From 2c1869be6d2be88ed47c70bb84df6bc973b1ee34 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:21:25 +0700 Subject: [PATCH 09/23] Build on izumo, the renamed window runtime tauri-runtime-blitz is published as izumo from 0.4.1, the release that answers agent-control requests instead of dropping the reply. The dependency, its three feature forwards, the runtime calls, the crates the owner build checks and the startup log all follow the new name. blitz-control-protocol moves to ^0.5.3, the version izumo is built against, so the two resolve to one copy. --- Cargo.toml | 2 +- apps/gui/Cargo.toml | 10 ++--- apps/gui/frontend/src/lib/theme.ts | 2 +- apps/gui/src/main.rs | 67 +++++++++++++++++++++--------- scripts/owner-build.sh | 2 +- 5 files changed, 56 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b3b20af..b4aee8fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ worktable = "1.10.0-beta1" # Tauri stack, so agent/proxy builds never trigger a webview toolchain build. # Local debugging only, alongside the path overrides in apps/gui/Cargo.toml. -# `tauri-runtime-blitz` takes blitz-dom and blitz-script by path but blitz-shell +# `izumo` takes blitz-dom and blitz-script by path but blitz-shell # and blitz-traits from git at the same rev. Cargo treats those as two distinct # crates, so the same trait exists twice and `ScriptDocument` stops satisfying # `Document`, which reads as eleven unrelated type errors inside a dependency diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index c73dfcd6..31d4e6d1 100644 --- a/apps/gui/Cargo.toml +++ b/apps/gui/Cargo.toml @@ -27,7 +27,7 @@ blitz-runtime = [ "dep:blitz-dom", "dep:blitz-script", "dep:brotli", - "dep:tauri-runtime-blitz", + "dep:izumo", "dep:url", ] # Compile the full diagnostics capability into the binary. The engine-wide @@ -52,10 +52,10 @@ blitz-inspector = [ # Fixed by `ps-anyrender-vello 0.14.1` and `ps-debug-timer 0.1.4`, both now # on crates.io. chuzz never hit either, because it does not enable this. "blitz-dom/log-phase-times", - "tauri-runtime-blitz/diagnostics", + "izumo/diagnostics", ] # The lighter render pipeline, for measuring against the default Vello one. -blitz-hybrid = ["blitz-inspector", "tauri-runtime-blitz/hybrid-renderer"] +blitz-hybrid = ["blitz-inspector", "izumo/hybrid-renderer"] [build-dependencies] brotli = { version = "8.0.4", default-features = false, features = ["std"] } @@ -107,8 +107,8 @@ tauri = { version = "2", default-features = false, features = ["macos-private-ap # 0.1.0 the runtime's own feature forwards to `tauri` and `tauri-runtime`, so # naming it here agrees with that rather than being the only thing holding the # two sides together. -tauri-runtime-blitz = { version = "^0.4", optional = true, features = ["macos-private-api"] } -blitz-control-protocol = { version = "^0.5", optional = true } +izumo = { version = "^0.4.1", optional = true, features = ["macos-private-api"] } +blitz-control-protocol = { version = "^0.5.3", optional = true } # # The engine by version, not by branch. Same move `chuzz` made, for the same # reasons its manifest records. diff --git a/apps/gui/frontend/src/lib/theme.ts b/apps/gui/frontend/src/lib/theme.ts index b6ec706f..c8398a9c 100644 --- a/apps/gui/frontend/src/lib/theme.ts +++ b/apps/gui/frontend/src/lib/theme.ts @@ -954,7 +954,7 @@ export function writePanelAxes(tuning: GlassTuning, root?: HTMLElement): void { * everything drawn and frosted the whole application, text included. That was * not a reason to give up the effect, it was the wrong attachment. * - * `tauri-runtime-blitz` now attaches an `NSGlassEffectView` as a *sibling* + * `izumo` now attaches an `NSGlassEffectView` as a *sibling* * below the renderer's view, in the window's content view, so it blurs what is * behind the window and the content draws over it untouched. * diff --git a/apps/gui/src/main.rs b/apps/gui/src/main.rs index 0856cfbc..15d7dac7 100644 --- a/apps/gui/src/main.rs +++ b/apps/gui/src/main.rs @@ -57,7 +57,7 @@ use tauri_plugin_dialog::DialogExt; use worktable::prelude::SelectQueryExecutor; #[cfg(feature = "blitz-runtime")] -pub(crate) type AppHandle = tauri::AppHandle; +pub(crate) type AppHandle = tauri::AppHandle; #[cfg(not(feature = "blitz-runtime"))] pub(crate) type AppHandle = tauri::AppHandle; @@ -997,7 +997,7 @@ fn resolved_highlights() -> String { let wanted = [ "ps-blitz-script", "ps-boa-engine", - "tauri-runtime-blitz", + "izumo", "@pathscale/ui", ]; // `cargo tree` marks a crate it has already expanded with a trailing @@ -1793,8 +1793,8 @@ pub(crate) async fn apply_settings_patch( || previous.blitz_deep_profiling_enabled != parsed.blitz_deep_profiling_enabled; #[cfg(feature = "blitz-runtime")] if runtime_debug_changed { - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::RuntimeDebugOptions { + izumo::apply_runtime_debug_options( + izumo::RuntimeDebugOptions { inspection_and_agent_control: parsed.blitz_control_enabled, deep_intrusive_profiling: parsed.blitz_deep_profiling_enabled, }, @@ -1805,8 +1805,8 @@ pub(crate) async fn apply_settings_patch( if let Err(error) = state.tables.kv_put(settings::KEY, merged.to_string()).await { #[cfg(feature = "blitz-runtime")] if runtime_debug_changed { - let _ = tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::RuntimeDebugOptions { + let _ = izumo::apply_runtime_debug_options( + izumo::RuntimeDebugOptions { inspection_and_agent_control: previous.blitz_control_enabled, deep_intrusive_profiling: previous.blitz_deep_profiling_enabled, }, @@ -2062,7 +2062,7 @@ fn ephemeral_location() -> location::DataLocation { #[cfg(all(feature = "blitz-runtime", target_os = "macos"))] #[tauri::command] fn set_window_chrome(tint: Option<[u8; 4]>, radius: Option, enabled: bool) { - tauri_runtime_blitz::set_window_glass(tint.map(|[r, g, b, a]| (r, g, b, a)), radius, enabled); + izumo::set_window_glass(tint.map(|[r, g, b, a]| (r, g, b, a)), radius, enabled); } /// Not macOS, or not the Blitz runtime: nothing to carry across. @@ -2463,12 +2463,41 @@ fn main() { * into a library for one tool's sake. */ if let Ok(spec) = std::env::var(qa_profile::ENV) { - let Some((source, destination)) = spec.rsplit_once(':') else { - eprintln!("{}: expected :", qa_profile::ENV); - std::process::exit(2); + // `:` still works, and both halves now have a + // default so the common case needs neither. + // + // Naming them by hand is the step that goes wrong: this build knows + // which identifier it uses and where its store is, and a caller typing + // the other one scrubs the wrong profile or writes a QA fixture over a + // real store. `AZ_BUILD_QA_PROFILE=1` takes this build's own store and + // writes the committed fixture path. + let (source, destination) = match spec.rsplit_once(':') { + Some((source, destination)) => ( + std::path::PathBuf::from(source), + std::path::PathBuf::from(destination), + ), + None => { + let identifier = if cfg!(feature = "experimental") { + "com.pathscale.agencyzero.experimental" + } else { + "com.pathscale.agencyzero" + }; + let Some(data_dir) = dirs::data_dir() else { + eprintln!("{}: no data directory for the default store", qa_profile::ENV); + std::process::exit(2); + }; + let source = data_dir.join(identifier).join("db"); + let destination = std::path::PathBuf::from( + concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/qa-profile"), + ); + println!( + "building from {} into {}", + source.display(), + destination.display() + ); + (source, destination) + } }; - let source = std::path::PathBuf::from(source); - let destination = std::path::PathBuf::from(destination); match nagoya::block_on(qa_profile::build(&source, &destination)) { Ok(rows) => { println!("scrubbed {rows} rows into {}", destination.display()); @@ -2496,7 +2525,7 @@ fn main() { // the native window actually did — whether a glass backdrop was applied, or // refused, and why. #[cfg(feature = "blitz-runtime")] - tauri_runtime_blitz::set_runtime_trace(|message| { + izumo::set_runtime_trace(|message| { crate::log!(log::Level::Info, "blitz", "{message}"); }); @@ -2536,7 +2565,7 @@ fn main() { })); #[cfg(feature = "blitz-runtime")] - tauri_runtime_blitz::set_document_factory(create_blitz_document); + izumo::set_document_factory(create_blitz_document); // The CLI switch is the rescue path for QA when the Settings toggle is // off. Read it before the app is built so control can start before the @@ -2548,7 +2577,7 @@ fn main() { std::env::args().any(|arg| arg == "--blitz-deep-profiling"); #[cfg(feature = "blitz-runtime")] - let builder = tauri_runtime_blitz::builder(); + let builder = izumo::builder(); #[cfg(not(feature = "blitz-runtime"))] let builder = tauri::Builder::default(); @@ -3012,8 +3041,8 @@ fn main() { .as_ref() .is_some_and(|settings| settings.blitz_deep_profiling_enabled); #[cfg(feature = "blitz-runtime")] - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::RuntimeDebugOptions { + izumo::apply_runtime_debug_options( + izumo::RuntimeDebugOptions { inspection_and_agent_control: blitz_control_enabled, deep_intrusive_profiling: blitz_deep_profiling_enabled, }, @@ -3300,8 +3329,8 @@ fn main() { // ps-qa gets a discovery descriptor even if native app activation stalls. #[cfg(feature = "blitz-runtime")] if cli_blitz_control_enabled || cli_blitz_deep_profiling_enabled { - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::RuntimeDebugOptions { + izumo::apply_runtime_debug_options( + izumo::RuntimeDebugOptions { inspection_and_agent_control: cli_blitz_control_enabled, deep_intrusive_profiling: cli_blitz_deep_profiling_enabled, }, diff --git a/scripts/owner-build.sh b/scripts/owner-build.sh index eb0325bd..7d8c1a64 100755 --- a/scripts/owner-build.sh +++ b/scripts/owner-build.sh @@ -80,7 +80,7 @@ if grep -q '^\[patch\.crates-io\]' Cargo.toml; then grep -A6 '^\[patch\.crates-io\]' Cargo.toml | sed 's/^/ /' fi -for crate in ps-blitz-script ps-boa-engine tauri-runtime-blitz; do +for crate in ps-blitz-script ps-boa-engine izumo; do line="$(cargo tree -i "$crate" --depth 0 2>/dev/null | grep -v '^warning' | head -1 || true)" [ -n "$line" ] || fail "$crate is not in the dependency graph" printf ' %s\n' "$line" From d96c26670cffbeb3132cc51b3c5d9498c5e40b3f Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:21:25 +0700 Subject: [PATCH 10/23] Take agency-proxy 0.1.11 The published 0.1.10 no longer compiles against endpoint-libs 3.2, which removed framed_json_with_max_frame, and the Rust job builds the sidecar from the resolved release, so every run failed there. 0.1.11 is built against 3.2. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b4aee8fd..3156cf72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ az-core = { path = "crates/core" } # time rather than at build time. Both are declared here so the pair cannot # drift: the crates inherit these, and sidecar staging asks cargo for the # resolved version instead of re-parsing the manifest to check they match. -agency-proxy-client = "^0.1.8" -agency-proxy-protocol = "^0.1.8" +agency-proxy-client = "^0.1.11" +agency-proxy-protocol = "^0.1.11" # The GUI and both storage tools compile the same schema against one resolved # WorkTable package. A workspace dependency prevents their compatible ranges # from drifting into separate copies without freezing the selected patch. From 1b95e0b72be61b80f1ee345752d15130454f558f Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 11:21:25 +0700 Subject: [PATCH 11/23] release: 0.8.66 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3156cf72..0540deac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ exclude = ["crates/wt-migrate/v2-reader"] [workspace.package] -version = "0.8.65" +version = "0.8.66" edition = "2024" publish = false From 783871579dfee246d683011b5148a6bbada07a79 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 18:56:45 +0700 Subject: [PATCH 12/23] Look for the izumo checkout, not tauri-runtime-blitz The repository and its local checkout are now izumo; local-renderer.sh refused to run because it checked for the old directory. --- scripts/local-renderer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/local-renderer.sh b/scripts/local-renderer.sh index 36cfaee1..9693fc4a 100755 --- a/scripts/local-renderer.sh +++ b/scripts/local-renderer.sh @@ -28,7 +28,7 @@ MSG exit 1 fi -for checkout in ps-anyrender ps-blitz tauri-runtime-blitz; do +for checkout in ps-anyrender ps-blitz izumo; do if [ ! -d "$root/../$checkout" ]; then echo "missing checkout: $root/../$checkout" >&2 echo "the redirect needs all three beside this repository" >&2 From b8d7c1a67f4b661d485fa280107212aa05897563 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 19:24:00 +0700 Subject: [PATCH 13/23] Say izumo where tauri-runtime-blitz was The runtime is published as izumo and its repository is pathscale/izumo. The docs, the QA notes and a script comment said tauri-runtime-blitz or TRB; they say izumo, and docs/trb-consumer-history is docs/izumo-consumer-history. Two lines that name a version only published under the old name keep it with "(now izumo)". --- docs/QA-audit-status-and-todo.md | 2 +- docs/TODO.md | 6 ++-- docs/allocations-plan.md | 4 +-- docs/allocations.md | 2 +- docs/build-graph-witnesses.md | 2 +- docs/concurrency-todo.md | 8 ++--- docs/driving-the-app.md | 8 ++--- docs/glass-support.md | 32 +++++++++---------- .../01-architecture.md | 6 ++-- .../02-plan.md | 4 +-- .../03-gaps.md | 0 .../04-risks.md | 0 .../05-implementation.md | 6 ++-- .../07-css-conformance.md | 0 .../README.md | 2 +- docs/partial-paint.md | 2 +- docs/performance.md | 4 +-- docs/zero-copy-and-hot-paths.md | 4 +-- scripts/check-one-rev-per-git-source.sh | 2 +- tests/ps-qa/issues.md | 6 ++-- tests/ps-qa/legacy-suite-migration.md | 2 +- 21 files changed, 51 insertions(+), 51 deletions(-) rename docs/{trb-consumer-history => izumo-consumer-history}/01-architecture.md (93%) rename docs/{trb-consumer-history => izumo-consumer-history}/02-plan.md (96%) rename docs/{trb-consumer-history => izumo-consumer-history}/03-gaps.md (100%) rename docs/{trb-consumer-history => izumo-consumer-history}/04-risks.md (100%) rename docs/{trb-consumer-history => izumo-consumer-history}/05-implementation.md (98%) rename docs/{trb-consumer-history => izumo-consumer-history}/07-css-conformance.md (100%) rename docs/{trb-consumer-history => izumo-consumer-history}/README.md (84%) diff --git a/docs/QA-audit-status-and-todo.md b/docs/QA-audit-status-and-todo.md index 91f63f09..50dfef07 100644 --- a/docs/QA-audit-status-and-todo.md +++ b/docs/QA-audit-status-and-todo.md @@ -9,7 +9,7 @@ Updated 2026-08-30. The repeatable procedure is - AgencyZero PR 201 unmounts every inactive top-level surface. It also keeps the legacy frontend unit suite manual and preserves typecheck, lint and the production frontend build as automatic CI. -- tauri-runtime-blitz 0.1.17 carries the node-addressed control protocol, +- tauri-runtime-blitz 0.1.17 (now izumo) carries the node-addressed control protocol, reusable glass support and the macOS availability fallback. Click activation targets the selected semantic node directly; coordinates do not select or retarget a control. diff --git a/docs/TODO.md b/docs/TODO.md index 97641275..ceaddd97 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -58,7 +58,7 @@ started. So one of these is true, and which one decides whether there is any work here: 1. The control-server fix - ([tauri-runtime-blitz 6173d83](https://github.com/pathscale/tauri-runtime-blitz)) + ([izumo 6173d83](https://github.com/pathscale/izumo)) closed it after all. A hung-up peer returned the same transport error forever and every client that ever disconnected left a task spinning. The note that "with the fix in, the 8.04s stands" was written from a binary @@ -629,7 +629,7 @@ lived in another process, so habits carried over from that build are mispriced. | 15 | **Pass Stylo a thread pool** (`main.rs:95`, `StyleThreading::Parallel`). We link Firefox's parallel style traversal and run it sequentially, because `DocumentConfig` defaults to `Sequential` and its own doc comment says the opposite. The multi-document hazard the comment warns about does not apply: one document, no iframes, one resolving thread. | 14 | [concurrency-todo.md](concurrency-todo.md) A2 | | 16 | **Partial.** `blitz-dom/parallel-construct` is enabled in `apps/gui/Cargo.toml`, so the existing rayon fan-out ships. Still open: a 0.6.4 on/off A/B for tab switching, shaping time, and RSS; item 19 also records the missing Genet pre-pass work rather than treating the feature flip as the complete optimization. | 14 | [concurrency-todo.md](concurrency-todo.md) A3 | | 17 | **Move the heavy read-only Tauri commands off the window thread** with `#[tauri::command(async)]`. 34 of 97 commands are non-async and therefore execute between two frames of the UI they serve, including `list_messages`, `list_task_log` and a filesystem walk in `list_table_sizes`. Audit call-order dependence first; leave the cheap ones and the writers sync. | none, but audit before edit | [concurrency-todo.md](concurrency-todo.md) C1, C2 | -| 18 | **Decide whether the renderer moves to its own thread**, fastrender's model: UI thread does OS events and message passing, renderer worker does the pipeline. Needs a `Send` audit and interacts with the main-thread id checks in `tauri-runtime-blitz`. Compositor-style scroll is the item after it and composes with item 12. | 14, 17 measured | [concurrency-todo.md](concurrency-todo.md) D1, D2 | +| 18 | **Decide whether the renderer moves to its own thread**, fastrender's model: UI thread does OS events and message passing, renderer worker does the pipeline. Needs a `Send` audit and interacts with the main-thread id checks in `izumo`. Compositor-style scroll is the item after it and composes with item 12. | 14, 17 measured | [concurrency-todo.md](concurrency-todo.md) D1, D2 | **Not on the list, with the reason:** parallel layout (no engine worth copying does it, and our cost is cache behaviour, which is item 7); moving DOM teardown to a worker (Boa and @@ -660,7 +660,7 @@ feature flip, and item 12 gains a prerequisite. **Items 22 to 26 are one class, not five unrelated checks.** We have hit "the build graph is not what we believed" four times and recorded each separately: `blitz-dom/incremental` absent and costing a measured 13x, `log-phase-times` shipping in release while being used to measure -it, `ps-anyrender-vello` reaching the app through `tauri-runtime-blitz` rather than where it +it, `ps-anyrender-vello` reaching the app through `izumo` rather than where it was looked for, and Genet's own fontconfig failure seen from outside. Genet asserts its architecture mechanically instead, and its wasm target check would have replaced the entire audit above with one command. The class, the prior art and the five traps are in diff --git a/docs/allocations-plan.md b/docs/allocations-plan.md index bfb36ee6..e2f1adb9 100644 --- a/docs/allocations-plan.md +++ b/docs/allocations-plan.md @@ -272,7 +272,7 @@ allocator cannot move the frame numbers much. But 78M of empty-but-resident malloc regions is a direct claim on the 855MB. There is no `#[global_allocator]` in `agencyzero`, `ps-blitz`, -`ps-anyrender` or `tauri-runtime-blitz`, so every allocation goes through macOS +`ps-anyrender` or `izumo`, so every allocation goes through macOS system malloc — the least favourable allocator for thousands of small, short-lived, single-threaded, LIFO-ish allocations per frame, which is exactly the shape `blitz-paint` produces. @@ -382,7 +382,7 @@ opaque number: `render()` minus paint. No new instrumentation was needed. `anyrender_vello`'s `render()` already contains a `debug_timer!` splitting the phases, behind a `log_frame_times` feature nothing enabled. Enabling it took one non-obvious step: the renderer -reaches the app through `tauri-runtime-blitz`, not through `ps-blitz-shell`, so +reaches the app through `izumo`, not through `ps-blitz-shell`, so a feature added anywhere in the ps-blitz workspace is never consulted. Naming `ps-anyrender-vello` directly in `apps/gui/Cargo.toml` under `blitz-inspector` is what reaches the copy actually built, because cargo unifies features across diff --git a/docs/allocations.md b/docs/allocations.md index 08599d0d..0190f798 100644 --- a/docs/allocations.md +++ b/docs/allocations.md @@ -60,7 +60,7 @@ short-lived, single-threaded, LIFO-ish allocations per frame. ### There is no custom global allocator -Grepped across `agencyzero`, `ps-blitz`, `ps-anyrender` and `tauri-runtime-blitz`: +Grepped across `agencyzero`, `ps-blitz`, `ps-anyrender` and `izumo`: no `#[global_allocator]`, no mimalloc, no jemalloc. All of the churn above goes through macOS system malloc, which is the least favourable allocator for that exact pattern. diff --git a/docs/build-graph-witnesses.md b/docs/build-graph-witnesses.md index 73aa9851..cdadbfbb 100644 --- a/docs/build-graph-witnesses.md +++ b/docs/build-graph-witnesses.md @@ -20,7 +20,7 @@ of anything: |---|---|---|---| | 1 | `blitz-dom`'s `incremental` feature was not in the build graph, so every `resolve` rebuilt the box tree and cleared the Taffy cache | a profile, weeks later | **13x**, measured. [performance.md](performance.md) calls it "the most fragile win here" | | 2 | `log-phase-times` sat on the base `blitz-dom` dependency line, so per-frame instrumentation shipped in release and sat inside every number taken with it | reading the manifest while writing [allocations.md](allocations.md) | every measurement to that date had the instrument in the baseline | -| 3 | `ps-anyrender-vello` reaches the app through `tauri-runtime-blitz`, not through `ps-blitz-shell`, so a feature added where it was expected was never consulted | a feature that "did not work" | [HANDOVER.md](HANDOVER.md) records `cargo tree -e features -i ` as the rule learned from it | +| 3 | `ps-anyrender-vello` reaches the app through `izumo`, not through `ps-blitz-shell`, so a feature added where it was expected was never consulted | a feature that "did not work" | [HANDOVER.md](HANDOVER.md) records `cargo tree -e features -i ` as the rule learned from it | | 4 | Two copies of one crate from two sources (path versus git, or git versus crates.io) | a wall of type errors inside a dependency nobody edited | the root `Cargo.toml` and `.cargo/config.toml` comments describe "eleven unrelated type errors" and "sixty type errors, all of them that" | Instance 4 is the only one that fails loudly, and even then it fails in the wrong place: diff --git a/docs/concurrency-todo.md b/docs/concurrency-todo.md index 5a07dc26..f1dd2c03 100644 --- a/docs/concurrency-todo.md +++ b/docs/concurrency-todo.md @@ -2,7 +2,7 @@ Written 2026-08-12. **Nothing here was measured.** It is a source review of this application against the engine checkouts it actually builds (`ps-blitz`, -`ps-taffy`, `ps-anyrender`, and the local `tauri-runtime-blitz`), read alongside Chromium, +`ps-taffy`, `ps-anyrender`, and the local `izumo`), read alongside Chromium, Gecko/WebRender, Stylo and fastrender. Where a number appears it is quoted from [performance.md](performance.md) or [HANDOVER.md](HANDOVER.md), which are the measured documents, and it says so. @@ -235,7 +235,7 @@ thread. In both, a slow frame is a late frame, not a frozen input queue. ### 2.7 The IPC boundary: 34 of 97 commands run inside the frame -`tauri-runtime-blitz/crates/tauri-runtime-blitz/src/ipc.rs:15` states the rule: "JavaScript +`izumo/crates/izumo/src/ipc.rs:15` states the rule: "JavaScript invokes the handler on the document's owning thread." The handler is installed at `:26` and calls Tauri's IPC handler inline. @@ -340,7 +340,7 @@ are not concurrency and belong to TODO items 8, 9 and the DOM list. **The honest answer to "can a worker do the cleanup".** No, not as stated, and not for a reason that a better design would fix cheaply. Boa and `blitz-dom` are deliberately -single-threaded (`tauri-runtime-blitz/.../script_queue.rs:10`: "`ScriptDocument` is +single-threaded (`izumo/.../script_queue.rs:10`: "`ScriptDocument` is intentionally single-threaded"). The JS heap, the DOM and the layout tree are one thread's data, so there is nothing separable to hand to a worker. `ScriptQueue::enqueue_task` (`script_queue.rs:67`) is the seam, and everything it accepts @@ -476,7 +476,7 @@ guesses. Measure with three unpaced runs and discard the first, per (`script_queue.rs:67`) is already a thread-safe ingress to the document thread. - **Why it is a decision and not a task:** it needs a `Send` audit of everything `View` owns, it interacts with `run_on_main_thread` in - `tauri-runtime-blitz/.../runtime.rs:274` and the main-thread id checks at `:278` and + `izumo/.../runtime.rs:274` and the main-thread id checks at `:278` and `:311`, and macOS window and accessibility APIs are main-thread only, so the split is not where a naive reading puts it. - **Depends on:** A and C being measured, so the decision is made against a real remaining diff --git a/docs/driving-the-app.md b/docs/driving-the-app.md index 00e95777..1de55db8 100644 --- a/docs/driving-the-app.md +++ b/docs/driving-the-app.md @@ -56,7 +56,7 @@ Four rules that each cost a session to learn: 3. **Check the instrument exists before building one.** Twice the timer being written already existed two crates away. `cargo tree -e features -i ` when a feature looks like it is not compiled in, because `ps-anyrender-vello` - reaches the app through `tauri-runtime-blitz` rather than `ps-blitz-shell`. + reaches the app through `izumo` rather than `ps-blitz-shell`. 4. **Layer counts ride the frame log, not the MCP surface.** `target/blitz-frame.log`, `layers_by_site=...`. Delete it before a run worth reading. @@ -237,7 +237,7 @@ cargo run -q -p blitz-bench -- nodes # tree size and role histogram ``` `blitz-bench` speaks the protocol through -[`blitz-control-protocol`](../../tauri-runtime-blitz/crates/blitz-control-protocol), +[`blitz-control-protocol`](../../izumo/crates/blitz-control-protocol), which is the **server's own** definition of the wire rather than a second copy of it. That is not tidiness: the Python hand-wrote this JSON and got the adjacent tagging of `AgentAction` wrong, which presented as a hung app, and its @@ -340,7 +340,7 @@ for a pushed stream, `waitForIdle`, and `snapshot`. not attributable to anything in particular. **Reading metrics perturbs the app.** The collection path spins the script loop -up to 100 times and forces a resolve (`tauri-runtime-blitz/src/runtime.rs:498` +up to 100 times and forces a resolve (`izumo/src/runtime.rs:498` and `:632`). Do not sample in a tight loop and then reason about the result. **All published numbers come from an inspector build.** Absolute figures carry @@ -363,7 +363,7 @@ If a profile stops naming functions, it was built without that override. ## Building without breaking the build The engine is consumed through local path checkouts patched in the root -`Cargo.toml`: `ps-blitz`, `tauri-runtime-blitz`, `ps-anyrender`. +`Cargo.toml`: `ps-blitz`, `izumo`, `ps-anyrender`. - `cargo fmt --check` gates the bundle build. Unformatted code in **any** of those checkouts fails the app build with a diff that looks unrelated to what diff --git a/docs/glass-support.md b/docs/glass-support.md index b59ace9f..70307255 100644 --- a/docs/glass-support.md +++ b/docs/glass-support.md @@ -68,7 +68,7 @@ The second can ship without the first and is where the existing groundwork is. `Color::TRANSPARENT` instead of the default opaque **white**) and Stage 3 (`apply_liquid_glass` with an `apply_vibrancy` fallback, plus `set_window_glass(tint, radius, enabled)` over a `OnceLock`) are all in - `tauri-runtime-blitz`. `window-vibrancy 0.8` was taken as a dependency after + `izumo`. `window-vibrancy 0.8` was taken as a dependency after all: it had already solved the view-hierarchy placement. - Stage 4 shipped as **three appearance sliders** — Panel lift, edge, depth — writing `--az-glass-lift`, `--az-glass-border` and `--az-glass-shadow`, with @@ -155,7 +155,7 @@ describe. Options, cheapest first: -1. **Add a `skia-renderer` feature to `tauri-runtime-blitz`**, beside the +1. **Add a `skia-renderer` feature to `izumo`**, beside the existing `hybrid-renderer`, selecting `SkiaWindowRenderer`. The seam is one `use` and one options type: `runtime.rs` already picks its renderer by feature. This is the only option where the blur is written already. @@ -270,7 +270,7 @@ nothing; it is now the more expensive one, and it brings C++ back. Two things still stand between this and glass on screen: -1. The `ps-blitz` and `tauri-runtime-blitz` pins have to move to the +1. The `ps-blitz` and `izumo` pins have to move to the `ps-anyrender` revision carrying it. 2. **`.rounded-panel` has no `backdrop-filter` declaration.** The only one in a current build is on `.modal__backdrop--blur`. Nothing will blur until the @@ -305,7 +305,7 @@ Two things still stand between this and glass on screen: [`types.rs:130`](../../ps-anyrender/crates/anyrender/src/types.rs), with a configurable `composite_alpha_mode` on the renderer options. - The window is already borderless-ish: `titleBarStyle: "Overlay"` with a - fullsize content view ([`runtime.rs:1815`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/runtime.rs)), + fullsize content view ([`runtime.rs:1815`](../../izumo/crates/izumo/src/runtime.rs)), so there is no native title bar to fight. ## The blockers, in the order they bite @@ -313,15 +313,15 @@ Two things still stand between this and glass on screen: ### 1. The window is always opaque `BlitzWindowBuilder` accepts `transparent` -([`lib.rs:178`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/lib.rs)) +([`lib.rs:178`](../../izumo/crates/izumo/src/lib.rs)) and stores it on the config. `window_attributes` -([`runtime.rs:1769`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/runtime.rs)) +([`runtime.rs:1769`](../../izumo/crates/izumo/src/runtime.rs)) then builds the winit `WindowAttributes` and **never calls `.with_transparent`**. The flag is accepted and dropped. `background_color` is the same story: the setter exists -([`lib.rs:218`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/lib.rs), -[`window_dispatch.rs:351`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/window_dispatch.rs)) +([`lib.rs:218`](../../izumo/crates/izumo/src/lib.rs), +[`window_dispatch.rs:351`](../../izumo/crates/izumo/src/window_dispatch.rs)) and nothing in `runtime.rs` reads it back. So today, `"transparent": true` in `tauri.conf.json` would be a silent no-op. @@ -409,10 +409,10 @@ these are already here transitively (`core-foundation 0.10.1`, Useful as a dependency we already have, not as the route to glass. **But it must be called on the winit window, from inside the runtime crate.** -`tauri-runtime-blitz` stubs the Tauri-facing handle out: -[`window_dispatch.rs:213`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/window_dispatch.rs) +`izumo` stubs the Tauri-facing handle out: +[`window_dispatch.rs:213`](../../izumo/crates/izumo/src/window_dispatch.rs) returns `Err(HandleError::NotSupported)` for `window_handle()`, and -[`runtime.rs:469`](../../tauri-runtime-blitz/crates/tauri-runtime-blitz/src/runtime.rs) +[`runtime.rs:469`](../../izumo/crates/izumo/src/runtime.rs) does the same for `display_handle()`. Calling `apply_liquid_glass` on a `tauri::Window` from `az-gui` therefore fails with `NoWindowHandle`. The winit `Window` implements the trait properly, so that is where the call goes — which @@ -492,7 +492,7 @@ clear the surface to transparent rather than to `background_color`. **Stage 3 — the effect view. Done, via `window-vibrancy` after all.** Enable the `NSGlassEffectView` / `NSVisualEffectView` features on the `objc2-app-kit` already in the graph, and insert the view behind the render -view on the winit window, from inside `tauri-runtime-blitz`. Gate on macOS 26 +view on the winit window, from inside `izumo`. Gate on macOS 26 with a `NSVisualEffectView` fallback. Not callable from `az-gui` — see blocker 2. Read `window-vibrancy`'s macOS module first for the view-hierarchy details, and fall back to depending on it if this fights winit. @@ -506,22 +506,22 @@ the stage most likely to look wrong in a hundred small ways. **Resolved by local `[patch]` entries**, the first option below. `.cargo/config.toml` in both `agencyzero` and `ps-blitz` now redirects `ps-blitz`, `ps-anyrender` and -`tauri-runtime-blitz` to the checkouts beside them. Those files encode absolute +`izumo` to the checkouts beside them. Those files encode absolute paths that exist on one machine, so they are **local only and must never be committed**; every pinned rev in `Cargo.toml` is still the truth for anyone else. The original reasoning follows. ### The original question -Stages 1–3 all live in **`tauri-runtime-blitz`**, and this app consumes it from a +Stages 1–3 all live in **`izumo`**, and this app consumes it from a **git rev** (`569870356`), not a path. Its local checkout at -`~/code/tauri-runtime-blitz` is on that exact commit but has a **dirty working +`~/code/izumo` is on that exact commit but has a **dirty working tree** (4 files, including `runtime.rs` and the control protocol) carrying unrelated deep-profiling work. So Glass cannot be implemented without either: -- adding a local `[patch]` for `tauri-runtime-blitz` — which pulls that dirty +- adding a local `[patch]` for `izumo` — which pulls that dirty work into the binary along with the glass change, or - committing or stashing that work first, or - landing the glass change upstream and moving the pinned rev. diff --git a/docs/trb-consumer-history/01-architecture.md b/docs/izumo-consumer-history/01-architecture.md similarity index 93% rename from docs/trb-consumer-history/01-architecture.md rename to docs/izumo-consumer-history/01-architecture.md index a1e0f0c8..3c632fff 100644 --- a/docs/trb-consumer-history/01-architecture.md +++ b/docs/izumo-consumer-history/01-architecture.md @@ -6,7 +6,7 @@ | # | Repo | Kind | Why | |---|---|---|---| -| 1 | `tauri-runtime-blitz` | **new** (this one) | Implements Tauri's `Runtime` trait over Blitz. Standalone crate, sibling to `tauri-runtime-wry`. | +| 1 | `izumo` | **new** (this one) | Implements Tauri's `Runtime` trait over Blitz. Standalone crate, sibling to `tauri-runtime-wry`. | | 2 | `blitz-rust` | **fork** of DioxusLabs/blitz | Carries `blitz-script` (the Boa integration, PR #491) which is an unmerged draft. Also where we fix DOM gaps. Cloned at `~/code/blitz-rust`, branch `js-engine`. | | 3 | `agencyzero` | existing, modified | Gains a cargo feature to select the runtime. No frontend changes expected. | | 4 | `@pathscale/ui` | existing, modified | Blitz-compatible variants for the CSS features Blitz lacks (see `03-gaps.md`). | @@ -27,7 +27,7 @@ tauri (2.11.5) upstream, unmodified | +-- tauri-runtime (2.11.3) upstream, trait definitions | - +-- tauri-runtime-blitz WE WRITE THIS + +-- izumo WE WRITE THIS | +-- blitz-dom fork: rendering +-- blitz-paint fork @@ -43,7 +43,7 @@ It owns the loopback WebDriver-compatible server described in `06-debug-control. talk to the renderer through a serialized command channel; the server thread must never touch Boa, the DOM, layout, or paint state directly. -## What tauri-runtime-blitz must implement +## What izumo must implement Against `tauri_runtime`'s traits (`Runtime`, `RuntimeHandle`, `WindowDispatch`, `WebviewDispatch`, `EventLoopProxy`): diff --git a/docs/trb-consumer-history/02-plan.md b/docs/izumo-consumer-history/02-plan.md similarity index 96% rename from docs/trb-consumer-history/02-plan.md rename to docs/izumo-consumer-history/02-plan.md index 88bc0456..5fc9d3da 100644 --- a/docs/trb-consumer-history/02-plan.md +++ b/docs/izumo-consumer-history/02-plan.md @@ -60,7 +60,7 @@ already runs headless). No Tauri, no IPC. Fix DOM gaps found in `03-gaps.md`. **Pass:** the app renders and is interactive against mock data. -## Stage 4 -- tauri-runtime-blitz +## Stage 4 -- izumo Only now write the runtime. IPC bridge first (unblocks everything), then windowing, then overlay title bar hit-testing, then menu passthrough. @@ -69,7 +69,7 @@ overlay title bar hit-testing, then menu passthrough. ## Stage 5 -- Ship behind a flag -Cargo feature selects `tauri-runtime-wry` (default) or `tauri-runtime-blitz`. Both ship. Flip +Cargo feature selects `tauri-runtime-wry` (default) or `izumo`. Both ship. Flip per-platform when Blitz is good enough. No big bang, no rollback risk. ## Non-goals diff --git a/docs/trb-consumer-history/03-gaps.md b/docs/izumo-consumer-history/03-gaps.md similarity index 100% rename from docs/trb-consumer-history/03-gaps.md rename to docs/izumo-consumer-history/03-gaps.md diff --git a/docs/trb-consumer-history/04-risks.md b/docs/izumo-consumer-history/04-risks.md similarity index 100% rename from docs/trb-consumer-history/04-risks.md rename to docs/izumo-consumer-history/04-risks.md diff --git a/docs/trb-consumer-history/05-implementation.md b/docs/izumo-consumer-history/05-implementation.md similarity index 98% rename from docs/trb-consumer-history/05-implementation.md rename to docs/izumo-consumer-history/05-implementation.md index 484262c3..3746fcdc 100644 --- a/docs/trb-consumer-history/05-implementation.md +++ b/docs/izumo-consumer-history/05-implementation.md @@ -13,7 +13,7 @@ Repos, already in place: - `~/code/blitz-rust` -- fork of DioxusLabs/blitz, on branch `js-engine` (PR #491 head) - `~/code/agencyzero` -- the app -- `~/code/tauri-runtime-blitz` -- this repo +- `~/code/izumo` -- this repo ## Stage 1 -- Solid on Boa @@ -128,7 +128,7 @@ Commit `17b2350f` adds synchronous and asynchronous remote JavaScript plus bound uncaught-error capture. Commit `a79b9ba7` adds pointer-path event traces, focused text entry through Blitz's IME/input path, and fixes empty inputs being initialized with a literal space. -This is a headless renderer result, not a `tauri-runtime-blitz` result. +This is a headless renderer result, not a `izumo` result. ## Stage 1.5 -- Reliable debug control @@ -232,7 +232,7 @@ Stage 3 passed on 2026-08-09. Order within Stage 4: renderer gaps; launch passed, appearance did not. - The preview can opt into the authenticated debug controller through the same environment and private descriptor contract as the headless harness. Normal Finder launches expose no port. -- The initial `tauri-runtime-blitz` crate preserves the real AgencyZero window configuration and +- The initial `izumo` crate preserves the real AgencyZero window configuration and connects Boa's `window.ipc.postMessage` host hook to Tauri's `WebviewIpcHandler`. - `BlitzWebviewDispatcher` implements Tauri's `eval_script` and `eval_script_with_callback` surfaces through a thread-safe queue drained by the owning Boa diff --git a/docs/trb-consumer-history/07-css-conformance.md b/docs/izumo-consumer-history/07-css-conformance.md similarity index 100% rename from docs/trb-consumer-history/07-css-conformance.md rename to docs/izumo-consumer-history/07-css-conformance.md diff --git a/docs/trb-consumer-history/README.md b/docs/izumo-consumer-history/README.md similarity index 84% rename from docs/trb-consumer-history/README.md rename to docs/izumo-consumer-history/README.md index 243e791b..2851b455 100644 --- a/docs/trb-consumer-history/README.md +++ b/docs/izumo-consumer-history/README.md @@ -2,7 +2,7 @@ These dated documents describe AgencyZero-specific renderer probes, gaps, risks, and rollout work. They live with the consuming application so -`tauri-runtime-blitz` remains application-agnostic. +`izumo` remains application-agnostic. Current QA procedure and results are maintained in `docs/QA-button-audit-runbook.md` and `tests/ps-qa/issues.md`. diff --git a/docs/partial-paint.md b/docs/partial-paint.md index 91e81895..19226bb0 100644 --- a/docs/partial-paint.md +++ b/docs/partial-paint.md @@ -33,7 +33,7 @@ in [performance.md](performance.md). This document is what that would mean concr | **GPU backend and present** | **`anyrender_vello` -> vello -> wgpu -> Metal** | **no** | | Frame loop, redraw cadence | `blitz-shell` | n/a, decides when | | Windowing, events | winit | n/a | -| Tauri runtime shim | `tauri-runtime-blitz` | n/a | +| Tauri runtime shim | `izumo` | n/a | Three consecutive layers own the whole-window property: diff --git a/docs/performance.md b/docs/performance.md index 231c983e..17084902 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -81,7 +81,7 @@ Each of these produced a confident, wrong conclusion before it was caught. Three different builds produced byte-identical PNGs with the same SHA256. Do not use it to judge visual correctness. 5. **`cargo fmt --check` gates the bundle build.** Unformatted code in any local - path checkout (`ps-blitz`, `tauri-runtime-blitz`, `ps-anyrender`) + path checkout (`ps-blitz`, `izumo`, `ps-anyrender`) fails the app build with a diff that looks unrelated to what you changed. 6. **Piping the build through `tail` discards its exit status.** The honest check is the binary's mtime, not the exit code you think you saw. @@ -408,7 +408,7 @@ the section above for the measurements. None of the above was visible before this work. Layout reported the cost of taking a snapshot as though it were the cost of a frame, with `scene`, `submit` -and `present` hardcoded to zero (`tauri-runtime-blitz/src/runtime.rs:689`), and +and `present` hardcoded to zero (`izumo/src/runtime.rs:689`), and script execution had no timing at all. - `blitz-shell/src/frame_stats.rs` publishes real per-frame timings with p95 and diff --git a/docs/zero-copy-and-hot-paths.md b/docs/zero-copy-and-hot-paths.md index 936970f7..3aff0bef 100644 --- a/docs/zero-copy-and-hot-paths.md +++ b/docs/zero-copy-and-hot-paths.md @@ -1,7 +1,7 @@ # Zero-copy, and what the hot paths actually copy Written 2026-08-11, from a read of the frontend store, `blitz-script`'s DOM bindings, -`blitz-dom`'s mutator, `tauri-runtime-blitz`'s script queue, Tauri 2.11.5's callback +`blitz-dom`'s mutator, `izumo`'s script queue, Tauri 2.11.5's callback formatter, and Boa's string implementation. **Nothing here was measured.** Every claim is a read of code, with file and line. Numbers quoted come from [performance.md](performance.md), taken 2026-08-10. @@ -80,7 +80,7 @@ For what step 8 costs beyond the copy, see Command responses and event payloads both return through `eval_script`, which pushes a `String` onto a queue that Boa evaluates -(`tauri-runtime-blitz/crates/tauri-runtime-blitz/src/script_queue.rs:16`, +(`izumo/crates/izumo/src/script_queue.rs:16`, `webview.rs:255`). Tauri decides how to encode the payload in `tauri-2.11.5/src/ipc/format_callback.rs`: diff --git a/scripts/check-one-rev-per-git-source.sh b/scripts/check-one-rev-per-git-source.sh index 95d3bc0f..5f3b81d1 100755 --- a/scripts/check-one-rev-per-git-source.sh +++ b/scripts/check-one-rev-per-git-source.sh @@ -13,7 +13,7 @@ # portability work — so a mismatched rev compiles green through every PR gate # and fails on the release job after the merge. That is exactly how 0.6.0 was # cut with a bundle that could not build: the app moved ps-blitz to 464444a2 -# and tauri-runtime-blitz was still asking for ada2f821. +# and izumo was still asking for ada2f821. # # Reading the lockfile rather than running cargo keeps it honest on any runner # and costs nothing, and the lockfile is the resolution the release job uses. diff --git a/tests/ps-qa/issues.md b/tests/ps-qa/issues.md index 9d107a30..8baeb7d9 100644 --- a/tests/ps-qa/issues.md +++ b/tests/ps-qa/issues.md @@ -12,7 +12,7 @@ current count comes from `ps-qa --app ps-qa.ron list --checks tests/ps-qa`. - AgencyZero PR 186, app 0.8.37 - ps-qa PR 10 candidate, 214 checks in 24 surface-grouped outcome areas - PathScale UI PR 262 candidate, package version 2.9.2 -- tauri-runtime-blitz candidate based on 0.1.5; the next release will include +- tauri-runtime-blitz (now izumo) candidate based on 0.1.5; the next release will include native, selected, pressed and checked state in one semantic boolean - disposable profile: `/tmp/qa-profile-db` - launch contract: `az-gui --blitz-control`; no descriptor environment variable @@ -76,7 +76,7 @@ present on any delivery branch. 2. The new-item editor used a nonstandard mount/blur sequence. It now uses an ordinary visible Input with explicit Enter/Escape behavior; a new row paints above every older row. -3. TRB exposed only `aria-selected`; pressed buttons and checked radios always +3. izumo exposed only `aria-selected`; pressed buttons and checked radios always reported false. The candidate now unifies native checked, `aria-selected`, `aria-pressed`, `aria-checked`, and option selection. 4. `ThemePicker` waited for slow settings round trips before reflecting button @@ -133,7 +133,7 @@ present on any delivery branch. 5/5. The keyed store update also mutates the stable item node instead of rebuilding the entire items array. 18. Dialog dismissal exposed a renderer crash in semantic inspection after a - popover removed a layout ancestor. TRB now rejects missing or cyclic layout + popover removed a layout ancestor. izumo now rejects missing or cyclic layout parent chains before geometry is computed and reports the node as not interactable instead of panicking. Dialog dismissal, Escape, welcome setup, and the isolated destructive fork outcome now pass 7/7. diff --git a/tests/ps-qa/legacy-suite-migration.md b/tests/ps-qa/legacy-suite-migration.md index e79b0094..cfa74265 100644 --- a/tests/ps-qa/legacy-suite-migration.md +++ b/tests/ps-qa/legacy-suite-migration.md @@ -81,7 +81,7 @@ physical-pointer dismissal, owner-change dismissal, and restore outcomes. The piece that would close it is a component identity in the semantic tree. A PathScale/UI component already names its parts in a recipe and emits them as -`data-slot`, but the tree TRB hands to ps-qa carries only `id`, `role`, `name`, +`data-slot`, but the tree izumo hands to ps-qa carries only `id`, `role`, `name`, `bounds`, `visible` and `value` - `data-slot` is dropped, and `ps-qa dom`'s `attrs:` column is the node's value, not its attributes. Verified against the running app: every node in the rename subtree reports `attrs: (none)`. From bc8f29b4344ebc1d2c75b1f68f2a73320209fa89 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 19:25:28 +0700 Subject: [PATCH 14/23] Build blitz-preview on izumo 0.4 blitz-preview still took tauri-runtime-blitz 0.3, the crate izumo was renamed from. It takes izumo ^0.4, and the ps-blitz crates move to ^0.4 with it: set_document_factory takes izumo's own ScriptDocument, so a second ps-blitz line would be a second, incompatible type. Every call maps one to one. The README and a comment that said the control socket was private and unfinished are brought up to date. --- apps/blitz-preview/Cargo.toml | 17 ++++++++++------- apps/blitz-preview/README.md | 6 +++--- apps/blitz-preview/src/main.rs | 14 ++++++++------ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/blitz-preview/Cargo.toml b/apps/blitz-preview/Cargo.toml index 3d09a792..d9aaef35 100644 --- a/apps/blitz-preview/Cargo.toml +++ b/apps/blitz-preview/Cargo.toml @@ -14,14 +14,17 @@ build = "build.rs" # with it the only genuinely headless path, from building at all. anyrender = { package = "ps-anyrender", version = "^0.13.0" } anyrender_vello_cpu = { package = "ps-anyrender-vello-cpu", version = "^0.16.0", optional = true } -blitz-dom = { package = "ps-blitz-dom", version = "^0.3", features = ["system-fonts"] } -blitz-paint = { package = "ps-blitz-paint", version = "^0.3", features = ["scrollbars"] } -blitz-script = { package = "ps-blitz-script", version = "^0.3", features = ["system-fonts"] } -blitz-traits = { package = "ps-blitz-traits", version = "^0.3" } +# The ps-blitz line has to match izumo's: `izumo::set_document_factory` takes +# a factory returning its own `ps-blitz-script` `ScriptDocument`, and a second +# release line here would be a second, incompatible `ScriptDocument` type. +blitz-dom = { package = "ps-blitz-dom", version = "^0.4", features = ["system-fonts"] } +blitz-paint = { package = "ps-blitz-paint", version = "^0.4", features = ["scrollbars"] } +blitz-script = { package = "ps-blitz-script", version = "^0.4", features = ["system-fonts"] } +blitz-traits = { package = "ps-blitz-traits", version = "^0.4" } brotli = { version = "^8.0.4", default-features = false, features = ["std"] } png = { version = "^0.18.1", optional = true } tauri = { version = "^2.11.5", default-features = false, features = ["compression"] } -tauri-runtime-blitz = "^0.3" +izumo = "^0.4" url = "^2.5.8" # For `--offscreen`: the activation policy has to be set before any window @@ -38,7 +41,7 @@ tauri-build = { version = "^2.6.3", features = [] } [dev-dependencies] # Use the renderer's SVG fork so geometry assertions inspect the exact tree # type produced by Blitz instead of compiling a second, incompatible `usvg`. -usvg = { package = "ps-usvg", version = "^0.48.1" } +usvg = { package = "ps-usvg", version = "^0.48" } [features] capture = ["dep:anyrender_vello_cpu", "dep:png"] @@ -47,7 +50,7 @@ capture = ["dep:anyrender_vello_cpu", "dep:png"] # `diagnosticsUnavailable`, which reads as a broken harness rather than a # missing feature. It is idle-cheap: the runtime flag still gates the work. default = ["diagnostics"] -diagnostics = ["tauri-runtime-blitz/diagnostics"] +diagnostics = ["izumo/diagnostics"] [profile.release] codegen-units = 1 diff --git a/apps/blitz-preview/README.md b/apps/blitz-preview/README.md index 67aaba01..eddcbfe0 100644 --- a/apps/blitz-preview/README.md +++ b/apps/blitz-preview/README.md @@ -1,7 +1,7 @@ # AgencyZero Blitz preview This is an isolated native preview of the production AgencyZero frontend on Blitz + Boa. It -uses the frontend's built-in mock backend, Tauri with `tauri-runtime-blitz`, winit windowing, +uses the frontend's built-in mock backend, Tauri with `izumo`, winit windowing, and Vello/wgpu rendering. On macOS, wgpu presents through Metal. The CPU renderer remains in the preview for deterministic headless captures and as a reference fallback. It does not link Wry or V8. @@ -22,5 +22,5 @@ until each AgencyZero command is exposed through the runtime. The normal release bundle excludes the CPU image renderer. Build or run with `--features capture` when deterministic PNG capture support is required. -The normal Finder launch opens no control port. Debug-control reintegration follows after the -concrete runtime window is stable. +The normal Finder launch opens no control port. Pass `--blitz-control` to open it, the same +flag az-gui uses. diff --git a/apps/blitz-preview/src/main.rs b/apps/blitz-preview/src/main.rs index fe0a8e0e..033b277b 100644 --- a/apps/blitz-preview/src/main.rs +++ b/apps/blitz-preview/src/main.rs @@ -7,6 +7,7 @@ use blitz_script::{DefaultScriptFetcher, FetchError, ScriptDocument, ScriptFetch #[cfg(all(not(test), feature = "capture"))] use blitz_traits::shell::{ColorScheme, Viewport}; use brotli::Decompressor; +use izumo::{builder, set_document_factory, set_runtime_trace}; #[cfg(not(test))] use std::fs::{self, OpenOptions}; use std::io::Read; @@ -16,7 +17,6 @@ use std::io::Write; use std::time::{SystemTime, UNIX_EPOCH}; // `Manager` brings `get_webview_window`, for the offscreen move below. use tauri::Manager; -use tauri_runtime_blitz::{builder, set_document_factory, set_runtime_trace}; use url::Url; include!(concat!(env!("OUT_DIR"), "/embedded.rs")); @@ -301,9 +301,11 @@ fn capture_preview(output: &std::path::Path) -> Result<(), String> { * * It is the same tree the inspector serves over the control socket * (`build_accessibility_tree`), so a headless run and a windowed run answer - * from one source. The socket itself is `pub(crate)` in - * `tauri-runtime-blitz` and so cannot be hosted from here, which is why - * this path writes the tree to a file instead of serving it. + * from one source. The socket used to be `pub(crate)` inside the runtime, + * which is why this path writes the tree to a file instead of serving it. + * It is public now, as `blitz-control-protocol`'s `server` module rather + * than anything in `izumo`, but serving a headless document is + * `qa-headless-host`'s job, so this path still writes a file. */ if let Some(tree_path) = std::env::var_os("AGENCYZERO_BLITZ_TREE") { let update = document.inner().build_accessibility_tree(); @@ -484,8 +486,8 @@ fn main() { * can be published without depending on native app activation. */ if std::env::args().any(|argument| argument == "--blitz-control") { - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::RuntimeDebugOptions { + izumo::apply_runtime_debug_options( + izumo::RuntimeDebugOptions { inspection_and_agent_control: true, deep_intrusive_profiling: false, }, From 6f780f77898488afed243ca97bbd3d57c4c030ac Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 19:25:48 +0700 Subject: [PATCH 15/23] Format blitz-preview after the rename The shorter crate name let rustfmt fold the debug-options call onto one line. --- apps/blitz-preview/src/main.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/blitz-preview/src/main.rs b/apps/blitz-preview/src/main.rs index 033b277b..3296b4c1 100644 --- a/apps/blitz-preview/src/main.rs +++ b/apps/blitz-preview/src/main.rs @@ -486,12 +486,10 @@ fn main() { * can be published without depending on native app activation. */ if std::env::args().any(|argument| argument == "--blitz-control") { - izumo::apply_runtime_debug_options( - izumo::RuntimeDebugOptions { - inspection_and_agent_control: true, - deep_intrusive_profiling: false, - }, - ) + izumo::apply_runtime_debug_options(izumo::RuntimeDebugOptions { + inspection_and_agent_control: true, + deep_intrusive_profiling: false, + }) .expect("could not enable Blitz control for the preview"); trace("blitz control enabled"); } From 5cf542b59c1078174113824fa63d7892eb23683e Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 19:26:05 +0700 Subject: [PATCH 16/23] Format apps/gui/src/main.rs cargo fmt --check failed on six spots: three the izumo rename shortened enough to fold, three older ones. --- apps/gui/src/main.rs | 49 +++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/apps/gui/src/main.rs b/apps/gui/src/main.rs index 15d7dac7..9308f029 100644 --- a/apps/gui/src/main.rs +++ b/apps/gui/src/main.rs @@ -994,12 +994,7 @@ const BUILD: BuildInfo = BuildInfo { /// that decide whether a rendering fix is present, so they go in the log where /// they are read without asking the app anything. fn resolved_highlights() -> String { - let wanted = [ - "ps-blitz-script", - "ps-boa-engine", - "izumo", - "@pathscale/ui", - ]; + let wanted = ["ps-blitz-script", "ps-boa-engine", "izumo", "@pathscale/ui"]; // `cargo tree` marks a crate it has already expanded with a trailing // `(*)`, so every package appears twice. Same version, no extra // information, and it makes the line read as though the graph were @@ -1793,24 +1788,20 @@ pub(crate) async fn apply_settings_patch( || previous.blitz_deep_profiling_enabled != parsed.blitz_deep_profiling_enabled; #[cfg(feature = "blitz-runtime")] if runtime_debug_changed { - izumo::apply_runtime_debug_options( - izumo::RuntimeDebugOptions { - inspection_and_agent_control: parsed.blitz_control_enabled, - deep_intrusive_profiling: parsed.blitz_deep_profiling_enabled, - }, - ) + izumo::apply_runtime_debug_options(izumo::RuntimeDebugOptions { + inspection_and_agent_control: parsed.blitz_control_enabled, + deep_intrusive_profiling: parsed.blitz_deep_profiling_enabled, + }) .map_err(|error| format!("could not update local Blitz debugging: {error}"))?; } if let Err(error) = state.tables.kv_put(settings::KEY, merged.to_string()).await { #[cfg(feature = "blitz-runtime")] if runtime_debug_changed { - let _ = izumo::apply_runtime_debug_options( - izumo::RuntimeDebugOptions { - inspection_and_agent_control: previous.blitz_control_enabled, - deep_intrusive_profiling: previous.blitz_deep_profiling_enabled, - }, - ); + let _ = izumo::apply_runtime_debug_options(izumo::RuntimeDebugOptions { + inspection_and_agent_control: previous.blitz_control_enabled, + deep_intrusive_profiling: previous.blitz_deep_profiling_enabled, + }); } if let Some(id) = boundary_id && let Err(cleanup) = state.tables.study_event.delete(id).await @@ -2483,13 +2474,17 @@ fn main() { "com.pathscale.agencyzero" }; let Some(data_dir) = dirs::data_dir() else { - eprintln!("{}: no data directory for the default store", qa_profile::ENV); + eprintln!( + "{}: no data directory for the default store", + qa_profile::ENV + ); std::process::exit(2); }; let source = data_dir.join(identifier).join("db"); - let destination = std::path::PathBuf::from( - concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/qa-profile"), - ); + let destination = std::path::PathBuf::from(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../target/qa-profile" + )); println!( "building from {} into {}", source.display(), @@ -3329,12 +3324,10 @@ fn main() { // ps-qa gets a discovery descriptor even if native app activation stalls. #[cfg(feature = "blitz-runtime")] if cli_blitz_control_enabled || cli_blitz_deep_profiling_enabled { - izumo::apply_runtime_debug_options( - izumo::RuntimeDebugOptions { - inspection_and_agent_control: cli_blitz_control_enabled, - deep_intrusive_profiling: cli_blitz_deep_profiling_enabled, - }, - ) + izumo::apply_runtime_debug_options(izumo::RuntimeDebugOptions { + inspection_and_agent_control: cli_blitz_control_enabled, + deep_intrusive_profiling: cli_blitz_deep_profiling_enabled, + }) .expect("could not apply CLI Blitz debugging"); } From 0f93afa2d9704681e1ef4c7779e002d0d02dda97 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 21:35:20 +0700 Subject: [PATCH 17/23] Take our own crates by major-line caret izumo, blitz-control-protocol, agent-experimental and promptsyntax were required at a patch floor. No lockfile is tracked, so the newest release resolves anyway; the floor only had to be chased on every release. --- apps/gui/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index 31d4e6d1..111166c4 100644 --- a/apps/gui/Cargo.toml +++ b/apps/gui/Cargo.toml @@ -67,9 +67,9 @@ wt-migrate = { path = "../../crates/wt-migrate" } agent-abstraction = "0.4.21" agency-proxy-client.workspace = true agency-proxy-protocol.workspace = true -agent-experimental = { version = "^0.1.3", default-features = false, optional = true } +agent-experimental = { version = "^0.1", default-features = false, optional = true } # Incremental authoring parsing and strict inertness diagnostics ship upstream. -promptsyntax = "0.2.0" +promptsyntax = "^0.2" # This workspace deliberately resolves without a lockfile. tinyvec 1.13.0 # fails its no_std heap branch because the vec macro is not imported, so keep # the compatible transitive graph on the last compiling release. @@ -107,8 +107,8 @@ tauri = { version = "2", default-features = false, features = ["macos-private-ap # 0.1.0 the runtime's own feature forwards to `tauri` and `tauri-runtime`, so # naming it here agrees with that rather than being the only thing holding the # two sides together. -izumo = { version = "^0.4.1", optional = true, features = ["macos-private-api"] } -blitz-control-protocol = { version = "^0.5.3", optional = true } +izumo = { version = "^0.4", optional = true, features = ["macos-private-api"] } +blitz-control-protocol = { version = "^0.5", optional = true } # # The engine by version, not by branch. Same move `chuzz` made, for the same # reasons its manifest records. From 1edc4dcaaeb0f02b1c48dafbf211165989209153 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 21:35:20 +0700 Subject: [PATCH 18/23] Own one nagoya reactor and pass it to agent-abstraction and the proxy client agent-abstraction 0.5 and agency-proxy 0.1.11 run on nagoya and take the reactor they register on as an argument rather than starting one of their own. The app creates the one Reactor during setup, hands its Handle to the AgencyProxy client and, through list_models, to model discovery, and keeps the Reactor in AppState for the life of the process: dropping it would stop the thread that reports those sockets and child processes. list_models takes the app state now; the catalogue itself is catalogues(), which the tests call with no reactor because nothing is discovered. The proxy's event receiver is agency-proxy-client's own broadcast type. --- apps/gui/Cargo.toml | 6 ++++-- apps/gui/src/agent_proxy.rs | 38 ++++++++++++++++++++++++++----------- apps/gui/src/main.rs | 20 ++++++++++++++++++- apps/gui/src/models.rs | 27 +++++++++++++++++--------- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index 111166c4..fb2c3e66 100644 --- a/apps/gui/Cargo.toml +++ b/apps/gui/Cargo.toml @@ -64,7 +64,7 @@ tauri-build = { version = "2", features = [] } [dependencies] # Carries the store forward when a column changes; see crates/wt-migrate. wt-migrate = { path = "../../crates/wt-migrate" } -agent-abstraction = "0.4.21" +agent-abstraction = "^0.5" agency-proxy-client.workspace = true agency-proxy-protocol.workspace = true agent-experimental = { version = "^0.1", default-features = false, optional = true } @@ -87,7 +87,9 @@ eyre = "0.6" rkyv = { version = "0.8.9", features = ["uuid-1"] } derive_more = { version = "2.0.1", features = ["from", "error", "display", "debug", "into"] } futures = "0.3" -nagoya = "^0.1" +# `reactor` for the one `Reactor` the app owns and hands to agent-abstraction, +# which registers the provider CLIs it spawns on it. +nagoya = { version = "^0.1", features = ["reactor"] } backon = { version = "1.6.0", default-features = false } libc = "0.2" dirs = "6" diff --git a/apps/gui/src/agent_proxy.rs b/apps/gui/src/agent_proxy.rs index fae63023..ec33de6d 100644 --- a/apps/gui/src/agent_proxy.rs +++ b/apps/gui/src/agent_proxy.rs @@ -5,6 +5,7 @@ //! normalized event vocabulary while the rest of the GUI is migrated. use agency_proxy_client::Client; +use agency_proxy_client::broadcast; use agency_proxy_protocol::{ ApprovalDecision, ClientMessage, ErrorCode, ProviderAccountUsage, ProviderStatus, RunEvent, RunId, RunRequest, RunSnapshot, ServerFrame, ServerResponse, ShutdownMode, @@ -21,7 +22,7 @@ use std::{ }, time::Duration, }; -use tokio::sync::{Mutex, broadcast}; +use tokio::sync::Mutex; static RUN_SEQUENCE: AtomicU64 = AtomicU64::new(1); @@ -52,6 +53,8 @@ impl ConnectionState { #[derive(Debug)] pub struct AgencyProxy { socket_path: PathBuf, + /// The reactor the client's socket is registered on, owned by the app. + reactor: nagoya::reactor::Handle, configured_binary: std::sync::RwLock>, start_gate: Mutex<()>, connection_state: Arc, @@ -71,9 +74,14 @@ pub struct Status { impl AgencyProxy { #[must_use] - pub fn new(config_dir: &Path, configured_binary: Option) -> Self { + pub fn new( + config_dir: &Path, + configured_binary: Option, + reactor: nagoya::reactor::Handle, + ) -> Self { Self { socket_path: proxy_socket_path(config_dir), + reactor, configured_binary: std::sync::RwLock::new(configured_binary), start_gate: Mutex::new(()), connection_state: Arc::new(AtomicU8::new(ConnectionState::Cold as u8)), @@ -516,7 +524,7 @@ impl AgencyProxy { } ConnectionState::Cold | ConnectionState::Live => {} } - if let Ok(client) = Client::connect(&self.socket_path).await { + if let Ok(client) = Client::connect(&self.socket_path, &self.reactor).await { self.clear_failure(); self.set_connection_state(ConnectionState::Live); return Ok(client); @@ -535,7 +543,7 @@ impl AgencyProxy { // Another caller may have connected or completed the initial spawn // while this one waited for the gate. Re-probe before deciding a // previously live daemon has really disappeared. - if let Ok(client) = Client::connect(&self.socket_path).await { + if let Ok(client) = Client::connect(&self.socket_path, &self.reactor).await { self.clear_failure(); self.set_connection_state(ConnectionState::Live); return Ok(client); @@ -640,7 +648,7 @@ impl AgencyProxy { // the dropped handle got wrong. let _ = watch_proxy_child(child, self.connection_state.clone()); for _ in 0..50 { - match Client::connect(&self.socket_path).await { + match Client::connect(&self.socket_path, &self.reactor).await { Ok(client) => { self.clear_failure(); self.set_connection_state(ConnectionState::Live); @@ -1109,7 +1117,7 @@ impl ProxyRun { loop { let frame = match self.events.recv().await { Ok(frame) => frame, - Err(broadcast::error::RecvError::Lagged(_)) => { + Err(broadcast::RecvError::Lagged(_)) => { let _ = self .client .request(ClientMessage::AttachRun { @@ -1119,7 +1127,7 @@ impl ProxyRun { .await; continue; } - Err(broadcast::error::RecvError::Closed) => { + Err(broadcast::RecvError::Closed) => { self.connection_state .store(ConnectionState::Crashed as u8, Ordering::Release); self.terminal = Some(Err("AgencyProxy connection closed".into())); @@ -1429,7 +1437,12 @@ mod tests { #[test] fn a_crash_keeps_the_specific_failure_for_settings_and_later_calls() { - let proxy = AgencyProxy::new(Path::new("/tmp/agency-proxy-failure-detail"), None); + let reactor = nagoya::reactor::Reactor::start().expect("reactor"); + let proxy = AgencyProxy::new( + Path::new("/tmp/agency-proxy-failure-detail"), + None, + reactor.handle(), + ); proxy.record_failure("socket bind failed: operation not permitted".into()); assert_eq!( proxy.failure_message(), @@ -1453,7 +1466,8 @@ mod tests { uuid::Uuid::now_v7() )); std::fs::create_dir_all(&dir).expect("create temp directory"); - let proxy = AgencyProxy::new(&dir, None); + let reactor = nagoya::reactor::Reactor::start().expect("reactor"); + let proxy = AgencyProxy::new(&dir, None, reactor.handle()); proxy.record_failure("could not start /nonexistent/agency-proxy".into()); assert_eq!(proxy.connection_state(), ConnectionState::Crashed); @@ -1496,7 +1510,8 @@ mod tests { uuid::Uuid::now_v7() )); std::fs::create_dir_all(&dir).expect("create temp directory"); - let proxy = AgencyProxy::new(&dir, None); + let reactor = nagoya::reactor::Reactor::start().expect("reactor"); + let proxy = AgencyProxy::new(&dir, None, reactor.handle()); // A crash first, which is the state the owner was actually in: the // daemon had been running and died. @@ -1530,7 +1545,8 @@ mod tests { uuid::Uuid::now_v7() )); std::fs::create_dir_all(&dir).expect("create temp directory"); - let proxy = AgencyProxy::new(&dir, None); + let reactor = nagoya::reactor::Reactor::start().expect("reactor"); + let proxy = AgencyProxy::new(&dir, None, reactor.handle()); let status = proxy .terminate() diff --git a/apps/gui/src/main.rs b/apps/gui/src/main.rs index 9308f029..f962a072 100644 --- a/apps/gui/src/main.rs +++ b/apps/gui/src/main.rs @@ -309,6 +309,14 @@ const IMPLEMENTED: &[&str] = &[ /// What the GUI carries for the life of the process. pub(crate) struct AppState { tables: Arc, + /// The one nagoya reactor this process owns, with a thread of its own. + /// + /// agent-abstraction registers the provider CLIs it spawns on a reactor + /// the caller passes in rather than one it starts for itself, so it lives + /// here, for the life of the app, and its handle goes to each call. + /// Dropping it would stop the thread that reports those children's pipes + /// and exits. + reactor: nagoya::reactor::Reactor, /// The threads az's own synchronous work runs on, owned rather than /// borrowed from tokio's process-wide blocking pool. See [`runtime::Pool`]. /// @@ -3043,7 +3051,16 @@ fn main() { }, ) .map_err(|error| format!("could not apply local Blitz debugging: {error}"))?; - let proxy = Arc::new(agent_proxy::AgencyProxy::new(&config_dir, configured_proxy)); + // The one reactor the app owns: the proxy client's socket and every + // CLI agent-abstraction spawns register on it. Kept in `AppState` + // below for the life of the process. + let reactor = nagoya::reactor::Reactor::start() + .map_err(|error| format!("could not start the nagoya reactor: {error}"))?; + let proxy = Arc::new(agent_proxy::AgencyProxy::new( + &config_dir, + configured_proxy, + reactor.handle(), + )); // A checkpoint backed by a still-live proxy run remains a live // draft. Only orphaned checkpoints become `interrupted` rows. let live_proxy_runs = tauri::async_runtime::block_on(proxy.list_runs()) @@ -3057,6 +3074,7 @@ fn main() { )); let restart_resume = take_restart_resume(&config_dir); app.manage(AppState { + reactor, tables: Arc::new(tables), pool: Arc::new(runtime::Pool::new()), proxy, diff --git a/apps/gui/src/models.rs b/apps/gui/src/models.rs index 8b1072e1..492e5cf3 100644 --- a/apps/gui/src/models.rs +++ b/apps/gui/src/models.rs @@ -98,14 +98,23 @@ pub fn verified_against(agent: Agent) -> String { /// the difference is visible in Settings. Failing the whole call instead would /// leave the picker with nothing over one agent's bad output. #[tauri::command] -pub async fn list_models(discover: bool) -> Vec { +pub async fn list_models( + state: tauri::State<'_, crate::AppState>, + discover: bool, +) -> Result, String> { + let handle = state.reactor.handle(); + Ok(catalogues(discover.then_some(&handle)).await) +} + +/// The catalogues, asking each agent that can be asked when `discover` names +/// the reactor to spawn it on, and reporting the verified list otherwise. +pub(crate) async fn catalogues(discover: Option<&nagoya::reactor::Handle>) -> Vec { let mut catalogues = Vec::with_capacity(AGENTS.len()); for agent in AGENTS { let verified = agent.models_verified(); - let discovered = if discover { - agent.discover_models().await.ok() - } else { - None + let discovered = match discover { + Some(handle) => agent.discover_models(handle).await.ok(), + None => None, }; let has_discovered = discovered.is_some(); catalogues.push(AgentModelsDto { @@ -129,7 +138,7 @@ mod tests { /// silently reverted would leave every model reading as not-default. #[tokio::test] async fn catalogues_serialize_in_the_shape_the_webview_expects() { - let catalogues = list_models(false).await; + let catalogues = catalogues(None).await; assert_eq!(catalogues.len(), AGENTS.len()); let json = serde_json::to_value(&catalogues).expect("should serialize"); @@ -155,7 +164,7 @@ mod tests { /// and naming the weaker evidence behind the compiled list. #[tokio::test] async fn a_compiled_catalogue_never_claims_to_have_been_discovered() { - for catalogue in list_models(false).await { + for catalogue in catalogues(None).await { assert!( !catalogue.discovered, "{:?} claimed discovery without being asked", @@ -167,7 +176,7 @@ mod tests { /// Exactly one preselection per agent, or the picker opens on nothing. #[tokio::test] async fn every_agent_offers_one_default() { - for catalogue in list_models(false).await { + for catalogue in catalogues(None).await { let defaults = catalogue.models.iter().filter(|m| m.is_default).count(); assert_eq!(defaults, 1, "{:?} should mark one default", catalogue.agent); } @@ -175,7 +184,7 @@ mod tests { #[tokio::test] async fn claude_opus_4_8_is_independently_selectable() { - let catalogues = list_models(false).await; + let catalogues = catalogues(None).await; let claude = catalogues .iter() .find(|catalogue| catalogue.agent == Agent::Claude) From df98233f1ca26bcbd597904497ba035b6869ffd9 Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 21:41:47 +0700 Subject: [PATCH 19/23] Declare the tokio macros az-gui uses az-gui calls tokio::select! and tokio::join! but never asked for the macros feature; the old agent-abstraction's tokio switched it on for the whole graph. With that dependency gone the release build stopped compiling, while the tests kept passing because the dev-dependency enables macros. --- apps/gui/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index fb2c3e66..9aa6a9ec 100644 --- a/apps/gui/Cargo.toml +++ b/apps/gui/Cargo.toml @@ -99,7 +99,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "serd # Unix sockets plus git/gh child processes also require Tokio's concrete I/O # types, so this compatibility boundary remains direct and declares every API # this crate calls instead of borrowing transitive features. -tokio = { version = "1", features = ["fs", "io-util", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } +tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } # Default features are off; see `webview-runtime` for why. tauri = { version = "2", default-features = false, features = ["macos-private-api", "compression"] } # `macos-private-api` must match the `tauri` dependency above. Tauri's From af4d09cc8841282c3ae584ed566d635d6d87bd4f Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 21:41:47 +0700 Subject: [PATCH 20/23] release: 0.8.67 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0540deac..26b4402a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ exclude = ["crates/wt-migrate/v2-reader"] [workspace.package] -version = "0.8.66" +version = "0.8.67" edition = "2024" publish = false From 168809d1e8156970cbb2b65960ddccfda5ced00e Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 23:55:08 +0700 Subject: [PATCH 21/23] Scroll the issue editor into view when it opens The editor mounts under its row, so a row near the bottom opened it below the fold: in CI's 1344x900 window the URL field laid out at y=936, focused but off screen, and `item-issue-editor-opens` failed. Focus moves no scrollport. Reveal the block holding the row and its editor, then focus. --- .../src/features/project/ProjectPanel.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/gui/frontend/src/features/project/ProjectPanel.tsx b/apps/gui/frontend/src/features/project/ProjectPanel.tsx index cb3c6f13..df07efb8 100644 --- a/apps/gui/frontend/src/features/project/ProjectPanel.tsx +++ b/apps/gui/frontend/src/features/project/ProjectPanel.tsx @@ -1197,6 +1197,24 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme const [savingIssueId, setSavingIssueId] = createSignal(null); let issueField: HTMLInputElement | undefined; + // The issue editor mounts under its row, so a row near the bottom of the + // list opens it below the fold: in a 1344x900 window the field laid out at + // y=936, focused but invisible, and the person who clicked saw nothing + // happen. `autofocus` does not help, because focusing moves no scrollport. + // Reveal the block that holds both the row and its editor: `nearest` on it + // brings the editor's bottom edge in without hiding the title it acts on. + createEffect( + () => issueDraft()?.item.id, + (id) => { + if (!id) return; + queueMicrotask(() => { + const row = document.querySelector(`[data-item-id="${id}"]`); + row?.parentElement?.scrollIntoView?.({ block: "nearest" }); + issueField?.focus(); + }); + }, + ); + const saveEdit = async (item: ProjectItem): Promise => { const value = editTitle().trim(); setEditingId(null); From 2b1d737b9e95e8097ab96c2e68557dfe3f3e8aeb Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 23 Sep 2026 23:55:08 +0700 Subject: [PATCH 22/23] Signal the watcher test's child from the test, not from a shell `sh -c 'kill -INT $$'` does not reliably die of the signal: the shell catches SIGINT and can reach exit first, and an inherited ignore cannot be undone. The watcher then saw an ordinary exit and the test failed about one run in seven. Send SIGINT from the parent to a sleep whose SIGINT disposition is reset to default across the exec. --- apps/gui/src/agent_proxy.rs | 51 ++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/apps/gui/src/agent_proxy.rs b/apps/gui/src/agent_proxy.rs index ec33de6d..85bb227c 100644 --- a/apps/gui/src/agent_proxy.rs +++ b/apps/gui/src/agent_proxy.rs @@ -1711,18 +1711,51 @@ mod tests { #[cfg(unix)] #[test] fn watching_an_intentionally_signaled_exit_leaves_stopped() { - let state = Arc::new(AtomicU8::new(ConnectionState::Live as u8)); - let child = Command::new("/bin/sh") - .arg("-c") - .arg("kill -INT $$") + use std::os::unix::process::CommandExt; + + // Not `sh -c 'kill -INT $$'`. A shell run with `-c` installs its own + // SIGINT catcher and only acts on it at its next checkpoint, so when + // `kill` is the last command it can reach exit first and end with a + // status instead of the signal. A shell that inherits SIGINT as + // ignored cannot un-ignore it at all. Either way the watcher saw an + // ordinary exit, left the state `Live`, and this failed about one run + // in seven while the code under test was right. + // + // So the signal comes from here, to a program that never handles it, + // with its disposition forced to default across the exec. `spawn` + // returns only after the exec succeeded, so the signal cannot land in + // the fork before the reset. The ten seconds bound a signal that was + // somehow lost: the test then fails on a status exit, it does not hang. + let mut command = Command::new("/bin/sleep"); + command + .arg("10") .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("spawn a shell"); + .stderr(Stdio::null()); + // SAFETY: `signal` is async-signal-safe, and nothing else runs between + // fork and exec. + unsafe { + command.pre_exec(|| { + if libc::signal(libc::SIGINT, libc::SIG_DFL) == libc::SIG_ERR { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + let child = command.spawn().expect("spawn a sleeper"); + let pid = libc::pid_t::try_from(child.id()).expect("pid fits pid_t"); + let state = Arc::new(AtomicU8::new(ConnectionState::Live as u8)); + let watcher = watch_proxy_child(child, state.clone()).expect("spawn watcher"); + // SAFETY: `pid` is our own unreaped child, held by the watcher's + // `wait`, so it cannot have been recycled for another process. + assert_eq!( + unsafe { libc::kill(pid, libc::SIGINT) }, + 0, + "signal the child: {}", + std::io::Error::last_os_error(), + ); - watch_proxy_child(child, state.clone()) - .expect("spawn watcher") + watcher .join() .expect("watcher finishes"); From a4e38fb4d3636a89fc5d9f39a54bad4a15a1ded4 Mon Sep 17 00:00:00 2001 From: meh Date: Thu, 24 Sep 2026 00:39:26 +0700 Subject: [PATCH 23/23] Format the watcher test --- apps/gui/src/agent_proxy.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/gui/src/agent_proxy.rs b/apps/gui/src/agent_proxy.rs index 85bb227c..04eee643 100644 --- a/apps/gui/src/agent_proxy.rs +++ b/apps/gui/src/agent_proxy.rs @@ -1755,9 +1755,7 @@ mod tests { std::io::Error::last_os_error(), ); - watcher - .join() - .expect("watcher finishes"); + watcher.join().expect("watcher finishes"); assert_eq!( ConnectionState::from_raw(state.load(Ordering::Acquire)),