Skip to content

Integrated browsing: shared browser policy crate, and a Browse surface in AgencyZero - #202

Open
pathscale wants to merge 4 commits into
masterfrom
feat/integrated-browsing
Open

Integrated browsing: shared browser policy crate, and a Browse surface in AgencyZero#202
pathscale wants to merge 4 commits into
masterfrom
feat/integrated-browsing

Conversation

@pathscale

@pathscale pathscale commented Aug 31, 2026

Copy link
Copy Markdown
Owner

What this is

H5: browsing integrated into AgencyZero, built as a shared library rather
than as a viewport, per the owner's direction: "chuzz browser engine should
ideally be a lib design so chuzz and az can share it. AZ will get a dedicated
viewing place kinda like Home tab."

Three commits, in order:

1. ps-browse-core — browser policy as a crate

Tabs, back/forward history, address-bar resolution and load outcomes, with
no engine dependency at all (serde and url, that is the whole set).

Chuzz already keeps browser policy separate from the renderer — it is the first
rule in its working agreement — but the separation is a module boundary inside
one 1700-line file in one binary, so AgencyZero could not reach it. This is the
same line drawn at a crate boundary.

The shape that makes it shareable is Load: the policy never fetches. It
answers "this tab, at this generation, now wants this URL", the host performs
the fetch with whatever net stack it has, and reports back. Nothing mentions a
document, a node or a window, which is why the whole thing is testable without
a compositor — 36 tests, milliseconds.

2. Host wiring in az-gui

apps/gui/src/browse.rs: Tauri commands over the policy, a blitz-net fetch
per navigation, and a poll hook on the chrome document that attaches the
fetched page to its <web-view> mount. A load carries the generation it was
issued at, so a page you navigated away from cannot arrive over the one you
asked for. A bundle whose mount has not been rendered yet is held rather than
dropped: a page that loads and renders nowhere is indistinguishable from one
that failed.

blitz-net and blitz-traits are added on the same ^0.3 line as blitz-dom
and blitz-script. They have to be — two engine versions in one graph put two
NetProvider traits in it.

3. The Browse surface

A pane beside Home: its own tab strip, address bar, history controls, a
debugging drawer, over the <web-view> mount. The expand control sits at the
left edge of the title bar, where the window's content begins, so it reads
as widening into a larger surface rather than as one more utility tab.

The mock implements the surface for real — tabs, history, the same address
policy — because this app is verified headlessly against the mock, and a mock
more permissive than the Rust would pass a test the app fails.

What chuzz adoption still needs

apps/chuzz/src/browser.rs still has its own copy of this policy. Replacing it
is mechanical but needs ps-browse-core published first: chuzz takes every
dependency as a caret range from crates.io, deliberately, so a path dependency
is not an option there. Nothing in the crate depends on AgencyZero, so that
flip is the only work standing between the two applications and one policy.
publish is left at the workspace default rather than flipped here, because
publishing a crate is not this PR's call to make.

Verified

  • cargo test -p ps-browse-core — 36 passed
  • cargo test -p az-gui browse — 4 passed
  • cargo check -p az-gui, cargo clippy, cargo fmt --all — clean
  • bun run typecheck — clean
  • bun run lint — clean, including the control-ownership contract
  • bun run test:run — 394 passed, 41 files

Not verified

Nobody has seen this on screen. No cargo tauri build has been run from
this clone and no window has been opened, so the layout, the mount, and whether
a page actually paints into the <web-view> are all unconfirmed. This machine
has no screen recording access, so a human has to look. The engine-side risk is
concentrated in one place: mount_node in browse.rs and the element id the
Browse pane renders have to agree, and when they drift the page loads correctly
and appears nowhere.

Design (H6) is not in this PR. The Browse pane is a tab kind, so a Design pane
slots in beside it without either owning the other's surface.

meh added 4 commits August 31, 2026 20:25
Tabs, back/forward history, address-bar resolution and load outcomes,
with no engine dependency: the policy answers "this tab, at this
generation, wants this URL" and the host does the fetching.

Chuzz already keeps browser policy separate from the renderer — it is the
first rule in its working agreement — but the separation is a module
boundary inside one binary, so agencyzero cannot reach it. This is the
same line drawn at a crate boundary.
Tauri commands over ps-browse-core, a blitz-net fetch per navigation, and
a poll hook on the chrome document that attaches the fetched page to its
<web-view> mount. The policy decides and the host fetches: a load carries
the generation it was issued at, so a page you navigated away from cannot
arrive over the one you asked for.

The mount is looked up on the UI thread, and a bundle whose mount has not
been rendered yet is held rather than dropped — a page that loads and
renders nowhere is indistinguishable from one that failed.
A browsing pane beside Home: its own tab strip, address bar, history
controls and a debugging drawer, over a <web-view> mount the engine
attaches pages to. The expand control sits at the left edge of the title
bar, where the window's content begins, so it reads as widening into a
larger surface rather than as one more utility tab.

The mock implements the surface for real — tabs, history, the same
address policy — because the app is verified headlessly against it, and a
mock more permissive than the Rust would pass a test the app fails. It
reports canRender: false, which is also how the chrome's no-renderer
state becomes reachable outside a webview-only build.
The JSX declaration for the page mount moves to a module of its own: a
declare module in a script file declares an ambient module that replaces
the real one, and in env.d.ts it made every JSX.Element in the app
resolve to nothing.

The address bar becomes Input.Field. The control contract bans the raw
HTML elements outright and requires every value-bearing control to be one
the QA suite already drives, so a raw input would have shipped an address
bar no rendered test could type into.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant