Add per-profile sync orchestration to `ely_browser_core`:
- `SyncEngine::for_profile_dir` loads / generates the persistent
device identity under `<profile_data>/sync/device.json` and reads
the bearer token from `<profile_data>/sync/bearer.token`.
- `install_bearer` accepts (or clears) the Better Auth session
token; everything else stays inert until a token is on disk.
- `upload_now(&BrowserCore)` serialises the user's bookmarks into a
stable JSON snapshot, ships it via `SyncApiClient::upload_snapshot`,
and remembers the resulting snapshot id / logical clock / device
for the UI to surface.
- `BrowserCore::visible_bookmarks_for_sync` returns a read-only view
the engine can iterate without touching the in-memory state.
The shell / settings-page wiring that calls `upload_now` ships
separately so this commit stays a pure model-layer change with no
runtime behaviour difference until the UI plugs in.
Build the Rust counterpart to `ely-browser-cloud`: a Bearer-token
authenticated HTTP client with the JSON wire types for the worker's
device + snapshot routes.
What lands:
- `BearerToken` + `BearerTokenStore` so Better Auth sessions persist
per profile data dir with atomic rename writes.
- `DeviceIdentity` (UUIDv7 + Ed25519-shaped public key, persisted
alongside the token so the worker keeps the same `device_id` across
restarts).
- `SyncApiClient` with `register_device`, `list_devices`,
`upload_snapshot`, and `download_snapshot` over `ureq`, mapping the
worker's strict error envelopes onto typed `SyncClientError`s.
- `SnapshotPayload` enforces the worker's 10 MiB / SHA-256-hash
contract before the wire encode, so callers fail fast.
Out of scope for this commit: the BrowserCore integration that swaps
snapshots in and out, and the in-app Better Auth + device-approval UX.
Those land in subsequent commits — `cloudflare/src/api_controls.ts`
rejects sync from devices that aren't already approved, so first-use
also requires a one-shot D1 approval until that path exists in the UI.
Servo already publishes the live page title in every `LiveFrameReport`
but the renderer was dropping it on the floor — tabs that navigated
away from `ely://new-tab` kept showing "New Tab" forever, and there
was no favicon visible anywhere in the sidebar.
Add `BrowserCore::set_tab_title` and switch `set_tab_favicon_key` to
return `Ok(true)` only when the value actually changed; both methods
mirror the new value into the matching history entry so the History
page stays in lockstep. Derive the canonical `/favicon.ico` URL from
the loaded URL on `UrlText` and store it as the tab's `favicon_key`.
In the surface layer, every Ready frame now emits a
`WebSurfacePageMetadata` change alongside any `WebSurfaceUrlChange`,
and the controller applies title + favicon URL together. Render the
sidebar tab row's favicon via GPUI's HTTP image loader (falling
through to the URL-derived glyph for `ely://` pages, file URLs, and
hosts without a /favicon.ico endpoint).
Root cause of the post-tab lag: the GPUI 16 ms timer was calling
`WebSurfaceRuntime::ensure_tab` and `tick` on the UI thread, and each
call did a synchronous `serde_json` write plus `read_line` against the
Servo sidecar over stdin/stdout. With even one visible tab, every
frame stalled on cross-process IPC.
Introduce `web_surface_worker.rs` — a per-profile worker thread that
owns the `ServoLiveClient`, drains a coalescing request queue
(latest Ensure/Poll per tab wins, no unbounded growth), and ships
results back through a `std::sync::mpsc` channel. `WebSurfaceRuntime`
now submits work non-blockingly and drains responses in `tick`; the
UI thread never blocks on the sidecar.
Adjacent in-flight cleanup riding along: hardware IOSurface
rendering-context completion (sidecar `live_protocol`,
`hardware_rendering_context`, GPUI BGRA surface shader), CSS viewport
size + device pixel ratio plumbing into `ServoLiveFrame`, and the
Send opt-ins for `CVPixelBuffer`-bearing types so frames can cross
the thread boundary.
`webview.paint()` dispatches a render command to Servo's paint thread
asynchronously, so the subsequent `read_to_image()` raced the paint
thread and reliably returned cleared-white pixels on data: URLs (T10.8).
Clear `has_pending_frame` before dispatching paint, then spin the Servo
event loop until `notify_new_frame_ready` re-arms it or 32 ms elapse
(override via `ELY_PAINT_BARRIER_MS`). Bench: software path now ships
real RGBA bytes for 60 frames instead of all-white.
T10.1: with the vendored `HardwareOffscreenContext` (048c5df) and the
host-level kind dispatch (a7d3e89) in place, the sidecar binary
still ignored the rendering context kind — every spawn was wired to
the software path regardless of how the host process was built. This
commit threads the choice end-to-end:
* `ely_servo_sidecar` learns a `--rendering-context [software|
hardware]` flag on its `live` subcommand. `LiveArgs` carries
the parsed `RenderingContextKind` (defaulting to `Software` so
existing invocations stay bit-identical) and `live.rs::run_live`
routes it through to `SoftwareServoHost::new_with_config_dir_and_kind`.
Unknown values produce a typed
`SidecarArgsError::InvalidRenderingContext`; a missing value
after the flag produces the existing `MissingArgumentValue`.
* `ely_app` reads `ELY_SERVO_RENDERING_CONTEXT` (with values
`software` / `hardware`, case-insensitive) and, if set, appends
`--rendering-context VALUE` to the sidecar command line.
Unset or unrecognised values fall through to the sidecar's own
software default — a stale env var or a typo never breaks the
browser startup. The sidecar arg parser is the source of truth
for legality of explicit values; the env helper only gates
which values reach it.
* Five new unit tests in `args::tests` pin the new parse paths:
default-is-software, explicit-software, explicit-hardware,
bogus-value-rejected, missing-value-rejected. Run via
`cargo test -p ely_servo_host --features servo-engine --bin
ely_servo_sidecar` and now hit alongside the five existing
snapshot tests for 10 passes.
End-to-end perf expectation: with the sidecar binary built using
`--features servo-engine,hardware-render` and the env var set to
`hardware`, every spawned sidecar webview rasterises through the
real GPU adapter (via the vendored
`HardwareOffscreenContext`/surfman/CGL chain on macOS). The host
still reads back RGBA into a `Vec<u8>` for the existing pipe
protocol; the IOSurface zero-copy bridge that deletes that
read-back is T10.2–T10.5 in docs/t10-iosurface-plan.md and lands
in subsequent commits.
cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.
cargo test -p ely_servo_host --features servo-engine --bin ely_servo_sidecar: 10 passed.
cargo test -p ely_servo_host --features servo-engine --test sidecar: 9 passed.
cargo test -p ely_servo_host --features servo-engine,hardware-render --test hardware_rendering_context: 1 passed.
`HardwareOffscreenContext` was vendored in 048c5df but the host's
per-webview `new_rendering_context` still hard-wired
`servo::SoftwareRenderingContext`. This commit threads a
`RenderingContextKind` enum through the host so existing call sites
keep their software path, and new callers can opt into the hardware
path through `SoftwareServoHost::new_with_config_dir_and_kind(...)`.
Three changes, kept tightly scoped:
* `runtime.rs` gains a public `RenderingContextKind { Software,
Hardware }` enum and a new constructor that takes it. The
existing `new` and `new_with_config_dir` keep their signatures
and default to `Software`, so the sidecar binary and the
integration tests pick up zero behavioural change. The
private `new_rendering_context` moves from a free function to a
`&self` method so it can read `self.rendering_context_kind` and
dispatch — `Software` constructs `SoftwareRenderingContext` as
before, `Hardware` constructs the vendored
`HardwareOffscreenContext`. When the `hardware-render` feature
isn't compiled in, the `Hardware` arm returns
`ServoHostError::HardwareRenderUnavailable` instead of silently
falling back; the new constructor also rejects the request
up-front before touching the global Servo runtime flag.
* `error.rs` gains `HardwareRenderUnavailable` so the wrong-feature
path is a typed error, not a panic.
* `lib.rs` exports `RenderingContextKind` alongside
`SoftwareServoHost` so downstream code (next commit will be the
sidecar's `--rendering-context` CLI flag and the live.rs
plumbing) can name the variant directly.
This is purely an extension point — no existing call path changes,
no existing test asserts on the new enum. The next commit will add
the sidecar CLI flag and wire `live.rs::run_live` to pass the kind
through to the host so users can pick the path at startup. The
follow-up commits then extract the IOSurface from the hardware
surfman surface and bridge it across the IPC channel to GPUI's
Metal renderer, deleting the host-side `Vec<u8>` from the per-frame
hot path entirely (full plan in docs/t10-iosurface-plan.md).
cargo test -p ely_servo_host --features servo-engine --lib: 2 passed.
cargo test -p ely_servo_host --features servo-engine --test sidecar: 9 passed.
cargo test -p ely_servo_host --features servo-engine,hardware-render
--test hardware_rendering_context: 1 passed.
cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.