Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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
Expand Down
17 changes: 10 additions & 7 deletions apps/blitz-preview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps/blitz-preview/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
20 changes: 10 additions & 10 deletions apps/blitz-preview/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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"));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
}
Expand Down
20 changes: 11 additions & 9 deletions apps/gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"] }
Expand All @@ -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.
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion apps/gui/frontend/src/features/project/MessageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions apps/gui/frontend/src/features/project/ProjectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,24 @@ function ItemList(props: { projectId: string; items: ProjectItem[] }): JSX.Eleme
const [savingIssueId, setSavingIssueId] = createSignal<string | null>(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<HTMLElement>(`[data-item-id="${id}"]`);
row?.parentElement?.scrollIntoView?.({ block: "nearest" });
issueField?.focus();
});
},
);

const saveEdit = async (item: ProjectItem): Promise<void> => {
const value = editTitle().trim();
setEditingId(null);
Expand Down
30 changes: 30 additions & 0 deletions apps/gui/frontend/src/features/project/inlineSpacing.test.ts
Original file line number Diff line number Diff line change
@@ -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 `<code>` 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");
});
});
19 changes: 19 additions & 0 deletions apps/gui/frontend/src/features/project/softWrap.ts
Original file line number Diff line number Diff line change
@@ -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 `<code>` 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, " ");
}
2 changes: 1 addition & 1 deletion apps/gui/frontend/src/lib/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading
Loading