diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7c4456e3..f86b8c73c 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 }} @@ -19,6 +25,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 +59,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 +109,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 @@ -110,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 diff --git a/Cargo.toml b/Cargo.toml index 6e230c6b6..26b4402a2 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.67" edition = "2024" publish = false @@ -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. @@ -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/blitz-preview/Cargo.toml b/apps/blitz-preview/Cargo.toml index 3d09a7929..d9aaef35d 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 67aaba017..eddcbfe0e 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 fe0a8e0eb..3296b4c1c 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,12 +486,10 @@ 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 { - 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"); } diff --git a/apps/gui/Cargo.toml b/apps/gui/Cargo.toml index c73dfcd6a..9aa6a9ec3 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"] } @@ -64,12 +64,12 @@ 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.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. @@ -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" @@ -97,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 @@ -107,7 +109,7 @@ 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"] } +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 diff --git a/apps/gui/frontend/src/features/project/MessageBody.tsx b/apps/gui/frontend/src/features/project/MessageBody.tsx index 903914673..91ef9c829 100644 --- a/apps/gui/frontend/src/features/project/MessageBody.tsx +++ b/apps/gui/frontend/src/features/project/MessageBody.tsx @@ -6,6 +6,7 @@ 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. @@ -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/ProjectPanel.tsx b/apps/gui/frontend/src/features/project/ProjectPanel.tsx index cb3c6f13d..df07efb84 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); 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 000000000..5aa4f85e2 --- /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 000000000..a52989a9f --- /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, " "); +} diff --git a/apps/gui/frontend/src/lib/theme.ts b/apps/gui/frontend/src/lib/theme.ts index b6ec706f1..c8398a9c4 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/agent_proxy.rs b/apps/gui/src/agent_proxy.rs index fae630233..04eee643f 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() @@ -1695,20 +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") - .join() - .expect("watcher finishes"); + watcher.join().expect("watcher finishes"); assert_eq!( ConnectionState::from_raw(state.load(Ordering::Acquire)), diff --git a/apps/gui/src/main.rs b/apps/gui/src/main.rs index 0856cfbce..f962a0725 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; @@ -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`]. /// @@ -994,12 +1002,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", - "tauri-runtime-blitz", - "@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 +1796,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 { - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::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 _ = tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::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 @@ -2062,7 +2061,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 +2462,45 @@ 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 +2528,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 +2568,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 +2580,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,14 +3044,23 @@ 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, }, ) .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()) @@ -3033,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, @@ -3300,12 +3342,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 { - tauri_runtime_blitz::apply_runtime_debug_options( - tauri_runtime_blitz::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"); } diff --git a/apps/gui/src/models.rs b/apps/gui/src/models.rs index 8b1072e1a..492e5cf35 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) diff --git a/apps/gui/src/update.rs b/apps/gui/src/update.rs index 9fabadd70..558b80963 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()) diff --git a/docs/QA-audit-status-and-todo.md b/docs/QA-audit-status-and-todo.md index 91f63f092..50dfef077 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 976412756..ceaddd974 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 bfb36ee6c..e2f1adb9c 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 08599d0d4..0190f798d 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 73aa98517..cdadbfbbe 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 5a07dc260..f1dd2c03f 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 00e95777d..1de55db82 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 b59ace9fd..70307255c 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 a1e0f0c8d..3c632fff4 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 88bc0456d..5fc9d3da9 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 484262c3f..3746fcdc3 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 243e791b5..2851b455f 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 91e81895c..19226bb03 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 231c983e4..17084902b 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 936970f7a..3aff0befd 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/button-sweep.sh b/scripts/button-sweep.sh index 8a2c6ba13..793442775 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 @@ -34,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 @@ -99,8 +113,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 diff --git a/scripts/check-one-rev-per-git-source.sh b/scripts/check-one-rev-per-git-source.sh index 95d3bc0f7..5f3b81d1d 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/scripts/local-renderer.sh b/scripts/local-renderer.sh index 36cfaee11..9693fc4a0 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 diff --git a/scripts/owner-build.sh b/scripts/owner-build.sh index eb0325bd8..7d8c1a648 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" diff --git a/tests/ps-qa/issues.md b/tests/ps-qa/issues.md index 9d107a30c..8baeb7d9a 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 e79b0094d..cfa742650 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)`.