Commit Graph
290 Commits
Author SHA1 Message Date
ZacharyZhang-NY 319b32b2de feat(sync-ui): animate sync controls 2026-05-16 00:19:59 -04:00
ZacharyZhang-NY 42740842b6 feat(chrome): animate high-frequency actions 2026-05-16 00:15:33 -04:00
ZacharyZhang-NY 40ab6b4874 feat(sync): round trip cloud snapshots 2026-05-16 00:01:28 -04:00
ZacharyZhang-NY 06e947f43e perf(web-surface): adapt shell tick cadence 2026-05-15 23:42:48 -04:00
ZacharyZhang-NY ca8f118785 perf(web-surface): throttle idle Servo polling 2026-05-15 23:25:09 -04:00
ZacharyZhang-NY a6620fa265 Fold the workspace picker into one pill and widen the traffic-light reserve
Drops the separate dashboard tile and the top-level "+" button so the
sidebar's first row holds a single workspace identity. "+ New
workspace" lives inside the disclosure popover next to the existing
spaces list, matching that row's geometry instead of inventing a new
square tile.

The previous 76 px traffic-light reserve only cleared the buttons
when the row also carried the tile and "+" — with the pill alone
plus its drop-shadow, it slid back behind the green button on the
default sidebar width. Widen `TRAFFIC_LIGHT_RESERVE` to 100 px and
document the derivation: pill outer left = `29 + RESERVE`, traffic-
light group right edge ≈ 112 px, so 100 leaves 17 px of breathing
room. Anchor solver and tests track the new geometry.
2026-05-15 23:02:53 -04:00
ZacharyZhang-NY 74b3de54ed Add email + OTP sign-in flow for cloud sync
Replace the "drop a session token in a file" workflow with a real
Chrome-style email login. The Cloudflare worker already had Better
Auth's `email-otp` plugin wired into `SEND_EMAIL`; this commit
builds the renderer-side counterpart.

Worker side:
- Move the OTP sender from `auth@elydora.com` to `browser@elydora.com`
  (wrangler.toml `allowed_sender_addresses` + better_auth.ts
  `EMAIL_OTP_FROM_ADDRESS`). Worker must be redeployed to pick this up.

Client side (`ely_sync_client::email_otp`):
- `send_email_otp(config, email)` POSTs `/api/auth/email-otp/send-verification-otp`
  with `{ email, type: "sign-in" }`.
- `verify_email_otp(config, email, otp)` POSTs `/api/auth/sign-in/email-otp`,
  reads the Better Auth session token from the JSON body's `token` field
  with the `Set-Cookie: better-auth.session_token=…` header as the
  documented fallback channel, and returns it as a `BearerToken`.

Shell side (`shell/auth.rs` + `shell/internal_pages/sync.rs`):
- New `AuthFlowPhase` (Idle / SendingCode / AwaitingOtp / Verifying /
  Error) tracks the in-flight form. Two off-thread workers run the
  HTTP exchanges so the GPUI render loop never blocks.
- Successful verify saves the bearer via `SyncEngine::install_bearer`
  and triggers an immediate snapshot upload, so the user is signed in
  + initial-synced in one click.
- Sync settings page replaces the bare "Sync now" button row with an
  account card: when SignedOut → email field + Send code → OTP field
  + Verify / Resend; when signed in → an account chip + Sign out.
- `trigger_cloud_sync_upload` no longer takes a `Context` param so
  the post-auth path can fire it from the inbox-drain pass without
  needing a window context.
2026-05-15 21:32:50 -04:00
ZacharyZhang-NY 80cff6dad3 Stop the web surface from strobing on transient errors
Two paths in `WebSurfaceStore::tick` were downgrading a perfectly good
Ready frame back to Failed / Loading on every transient hiccup:

- `WebSurfaceRuntimeFrame::Failed` overwrote the surface state
  unconditionally. Combined with the 125 Hz tick (which submits a Poll
  for every visible tab and any transient ensure / poll error becomes
  a Failed response), even one parse glitch wiped the rendered page.
- The `initial_display_gate_message` and `should_hold_initial_frame`
  checks for incoming Ready frames hardcoded `has_previous_frame =
  false`, so a stray empty paint pass after the page had already
  rendered would knock the surface back to Loading or Failed.

Detect whether the surface already has a `Ready(_)` state and:
- ignore Failed responses (logging through `tracing` for diagnostics)
  while a real frame is on screen;
- pass that "had ready" signal into the gate / hold checks so they only
  apply to the very first paint, not later refreshes.

The page now stays put even when Servo emits a momentary error; only
the first-paint failure path can mark the surface Failed.
2026-05-15 21:21:14 -04:00
ZacharyZhang-NY 838f80006a Remove broken sidebar Layout cards from appearance settings
The three layout preset cards (Single column / Compact / Hidden on
hover) painted as bright white boxes regardless of theme, never
implemented the layouts they previewed, and duplicated the live
sidebar resize handle which already covers the same intent. Drop
the section, the supporting `appearance_layout_cards` module, and
the chrome `mod` declaration that re-exported it.
2026-05-15 21:18:10 -04:00
ZacharyZhang-NY 81a75f6489 Inline the macOS traffic lights with the workspace picker
The sidebar header stacked a redundant `ELY Browser ⌄` title row above
the workspace picker, pushing the picker down a full 30 px and leaving
the traffic lights stranded over the title text. macOS already advertises
the app name in the menubar, so the in-window title is dead weight.

Drop the title row, reserve `TRAFFIC_LIGHT_RESERVE = 76 px` at the start
of the picker row instead, and recompute the disclosure anchor from the
new geometry. Traffic-light Y from main.rs (`SHELL_INSET + 22`) lines
up with the new picker row's vertical center on the first frame, so the
anchor solver sums one fewer row.
2026-05-15 21:16:10 -04:00
ZacharyZhang-NY 39d10bf3d7 Convert chrome-local color constants to theme-aware accessors
The previous theme refactor only flipped `colors::*` tokens; the
~50 chrome-local `const FOO: u32 = 0xffffff…;` overlays kept their
white-glass values, so dark mode still painted a light omnibar pill,
light search field, light sidebar tab card, light command-overlay
backdrop, light sync page chips, etc.

Promote `colors::pick(light, dark)` to `pub` so component-internal
constants can reach the same thread-local mode the global palette
uses, then convert every white-glass (`0xffffff??`) and warm-dark
wash (`0x281e14??`) constant in:

- chrome/topbar (omnibar pill, hover chip)
- chrome/sidebar_chrome + chrome/sidebar (rail nav row hover/active,
  highlight border, close-button hover, unread badge)
- chrome/sidebar_header (workspace picker, disclosure card)
- chrome/command_overlay + chrome/command_rows + chrome/command_footer
- chrome/settings_layout, chrome/appearance_form,
  chrome/appearance_layout_cards, chrome/plugin_detail_view
- chrome/home/style (search field, arrow chip, pill, card, add-tile)
- internal_pages/sync (status pill, metrics card, button row)
- internal_pages/plugin_catalog
- shell/render (main pane highlight border)

Each constant becomes `fn name() -> u32 { colors::pick(<light>, <dark>) }`
where the dark counterpart preserves the original alpha but swaps the
warm-white prefix `ffffff` for warm-graphite `1f1d1b` (or the inverse:
`281e14` warm-dark wash → `f2efe9` warm-white wash). Brand accents
(`0xc96442??`, `0xffaa7733`) and the global slate backdrop keep their
single value across modes.
2026-05-15 20:22:51 -04:00
ZacharyZhang-NY 467dcb1f87 Reflect real sync state on the Sync settings page
`SyncConnectionState` was a one-variant enum (`SignedOut`), so the
Sync page rendered "Local-only · sign-in coming soon" even after the
bearer token landed on disk and the upload thread completed. The
state machine now mirrors the actual lifecycle.

What lands:
- `SyncConnectionState` gains `SignedIn`, `AwaitingDeviceApproval`,
  `SyncReady { last_synced_at_secs }`, `SyncError { message }`.
  `SyncObjectState::Synced` joins the per-object enum so individual
  rows can advertise "Synced" once a successful upload lands.
- `BrowserCore` stores the current `SyncConnectionState` and exposes
  `set_sync_connection_state`. `sync_status` now propagates the live
  state into the snapshot the UI reads.
- `ElyShell::probe_initial_sync_state` inspects
  `<profile_data>/sync/bearer.token` synchronously at construction
  so the first render of the sync page is honest about whether the
  user is signed in.
- A `std::sync::mpsc` channel ferries upload outcomes from the
  off-thread worker back to the shell; the existing 8 ms tick
  drains it and stamps `core.set_sync_connection_state` with the
  freshest result. The UI now shows "Signed in · awaiting first
  sync", "Synced · last upload Xm ago", "Sync error · …", and the
  worker-special "Signed in · waiting for device approval" when the
  server returns `device_not_approved`.
2026-05-15 19:58:42 -04:00
ZacharyZhang-NY 6b2578c3a8 Make every shell color token theme-aware
Dark mode was persistent in `AppearanceSettings` but never reached any
paint code: every call site in the shell read `colors::INK` etc. as a
`pub const u32`, so toggling `ThemeMode::Dark` mutated state nothing
sampled. Root-cause fix is to invert the contract — the design-system
exports functions that resolve through a thread-local `Mode`, and the
GPUI render impl sets that mode each frame.

What lands:
- `ely_design_system::colors::Mode` + thread-local + `set_mode` /
  `mode` accessors. Every ink shade, glass surface, stroke, divider,
  hairline, canvas, success / error chip now picks the warm-dark
  counterpart when the active mode is `Mode::Dark`.
- `ElyShell::render` resolves `ThemeMode::System` against
  `Window::appearance()` and pushes the mode before traversing the
  tree, so widgets lower down read the right shade without owning a
  `Mode` parameter.
- `render_wallpaper` + `panel_bg` now branch on `colors::mode()` so
  the gradient base, panel tint, and overlay highlights flip to
  warm-graphite when dark mode is active.
- Mechanical conversion across 687 call sites in 55 files from
  `colors::FOO` constants to `colors::foo()` accessors. The
  `Theme` / `ELY_THEME` const surface (unused outside the design
  system) is removed; the function surface is the new contract.
2026-05-15 19:50:38 -04:00
ZacharyZhang-NY 5548427df3 Land 125 Hz Servo polling and eased shell transitions
Now that `LiveRuntimeWorker` does all the blocking IPC off the UI
thread, the 16 ms shell tick is no longer the bottleneck — drop it to
8 ms (≈ 125 Hz) so a 120 Hz display can present a fresh Servo frame
between every refresh. The worker queue still coalesces, so doubling
the rate does not double the wire traffic.

Replace `fade_in`'s linear ramp with an ease-out cubic so panel /
overlay reveals decelerate the way the design tokens promise instead
of cutting in abruptly at the end. Unit-tests pin the curve shape so a
future refactor that wires in a different easing function won't
silently revert to linear.
2026-05-15 17:19:38 -04:00
ZacharyZhang-NY 6bacb3faa8 Add Sync now button that uploads a bookmarks snapshot
Wire `SyncEngine::upload_bytes` to a Settings → Sync button:

- `BrowserCore::build_sync_snapshot_bytes` serialises the user's
  bookmarks on the UI thread (cheap, synchronous).
- `ElyShell::trigger_cloud_sync_upload` resolves the active
  profile data dir, spawns a dedicated `ely-sync-upload` thread,
  and lets the engine run the blocking HTTP round-trip there so
  the GPUI render loop never stalls on the network — the same
  invariant the Servo IPC worker enforces.
- Outcomes go through `tracing` on the `ely::sync` target. Users
  drop a Better Auth bearer token into
  `<profile_data>/sync/bearer.token` to opt in; without one, the
  engine reports `SignedOut` and the click is a no-op.

The Better Auth handshake + device-approval UX still need their
own UI passes; this lands the data-plane plumbing so those pieces
slot in without re-architecting the snapshot path.
2026-05-15 17:15:07 -04:00
ZacharyZhang-NY 3d2c3ed1bf Propagate page title and favicon into the active tab
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).
2026-05-15 16:53:32 -04:00
ZacharyZhang-NY 00a8ff1fef Apply cargo fmt across chrome and internal pages 2026-05-15 16:42:00 -04:00
ZacharyZhang-NY 90c029eddb Move Servo IPC off UI thread
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.
2026-05-15 16:41:40 -04:00
ZacharyZhang-NY f4c650c4d8 Fix Servo IOSurface orientation and resize identity 2026-05-13 10:21:50 -04:00
ZacharyZhang-NY 8238526cc1 Wait for complete live-site smoke frames 2026-05-13 04:40:14 -04:00
ZacharyZhang-NY 2302a53b39 Cover web surface live scrolling 2026-05-13 04:31:58 -04:00
ZacharyZhang-NY d4046ca4e7 Default Servo live rendering to hardware on macOS 2026-05-13 04:14:18 -04:00
ZacharyZhang-NY a743435ea1 Transfer Servo IOSurface ports over Mach 2026-05-13 04:05:07 -04:00
ZacharyZhang-NY 72f7b9761e Report Servo live IOSurface import failures 2026-05-13 03:30:55 -04:00
ZacharyZhang-NY ac84a80d9e Close Servo live sessions for removed tabs 2026-05-13 02:41:54 -04:00
ZacharyZhang-NY 8fb1277ced Default Servo live rendering to software 2026-05-13 02:13:51 -04:00
ZacharyZhang-NY e7b62e2daf Scope Servo live runtimes by profile 2026-05-13 02:07:01 -04:00
ZacharyZhang-NY 05ce4afba4 Poll only visible web surfaces 2026-05-13 01:54:51 -04:00
ZacharyZhang-NY 6ce5df83a8 Skip hardware live readback 2026-05-13 01:41:58 -04:00
ZacharyZhang-NY c19e6923c3 Clean gpui harness clippy debt 2026-05-13 01:26:52 -04:00
ZacharyZhang-NY 5bdb2871ad Invert wheel deltas for Servo scroll 2026-05-13 01:20:10 -04:00
ZacharyZhang-NY 1325f043b4 Commit Servo loaded URLs to tabs 2026-05-13 01:09:39 -04:00
ZacharyZhang-NY 4cb7ff089e Wire topbar tab history navigation 2026-05-13 00:49:52 -04:00
ZacharyZhang-NY 777df012e3 Validate Servo hardware frame surfaces 2026-05-13 00:29:26 -04:00
ZacharyZhang-NY 15d773b28e Prefer Cargo hardware sidecar only for workspace builds 2026-05-13 00:25:48 -04:00
ZacharyZhang-NY 9313c76743 Default Servo live rendering to hardware on macOS 2026-05-13 00:24:39 -04:00
ZacharyZhang-NY 05a7a0d67f Present Servo BGRA hardware surfaces 2026-05-13 00:22:00 -04:00
ZacharyZhang-NY bf1ebfb6fe Hold hardware live mode until BGRA presentation 2026-05-12 23:42:52 -04:00
ZacharyZhang-NY 316bf6f9a4 Split Servo live rendering modules 2026-05-12 23:38:13 -04:00
ZacharyZhang-NY 0db1caad69 Route web surface scrolls to Servo hit point 2026-05-12 23:29:40 -04:00
ZacharyZhang-NY 5269853ec8 T14: disable T10.5 surface() path; GPUI 0.2.2 Surface asserts NV12 YUV 2026-05-11 00:53:18 -04:00
ZacharyZhang-NY b70fa71a9f T13: push display scale factor into Servo's hidpi so Retina pages lay out at logical CSS dimensions 2026-05-11 00:46:32 -04:00
ZacharyZhang-NY 5d8734aefc T11: swap R↔B between Servo RGBA and GPUI BGRA so colours stop inverting 2026-05-11 00:27:09 -04:00
ZacharyZhang-NY 6e2bd20a9b T10.6: drop RGBA payload from the wire when the hardware path publishes a surface 2026-05-10 23:52:12 -04:00
ZacharyZhang-NY a447d52262 T10.5: render IOSurface via gpui::surface(CVPixelBuffer) 2026-05-10 23:37:00 -04:00
ZacharyZhang-NY c4a6ea3c57 T10.4: import sidecar IOSurface mach port into a cached MTLTexture 2026-05-10 23:27:56 -04:00
ZacharyZhang-NY 07b9c9da01 T10.3: publish IOSurfaceHandle once per surface, current_surface_id per frame 2026-05-10 23:17:05 -04:00
ZacharyZhang-NY f7027e6ea5 Profile frame stages via sidecar histogram → ely::servo::perf 2026-05-10 22:30:57 -04:00
ZacharyZhang-NY 18fd20b577 Plumb the rendering context kind from ely_app env var to the sidecar
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.
2026-05-10 21:27:12 -04:00
ZacharyZhang-NY 3f184ee941 Swap SipHash13 → AHash for the live-frame dedup key + record T10 plan
Two T10-flavoured changes in one commit, each independently ship-able
on its own:

1. `web_surface_frame::rgba_hash` switches from std's
   `DefaultHasher` (SipHash13, ~1.5 GB/s) to `ahash::AHasher`
   (~10 GB/s). At 1080p (8 MB per frame) the dedup key drops from
   ~5 ms to ~0.8 ms per cache-miss frame, returning roughly 25 % of
   the 16 ms scroll budget that was being spent hashing the
   newly-arrived RGBA payload. ahash was already in the dependency
   graph transitively via hashbrown, so this only adds a direct
   `ahash = "0.8"` line and one Cargo.lock entry.

2. `docs/t10-iosurface-plan.md` records the full architectural
   roadmap for the actual zero-copy path that supersedes the
   software-pipe pipeline: `OffscreenRenderingContext` against a
   hardware surfman adapter, IOSurface-backed surface on macOS,
   mach-port handoff to the GPUI process, MTLTexture import as an
   external sampler. The document explains why each currently
   shipped commit (`840255f`, `a80d039`, `e02c0fd`, `7f3b8b4`, plus
   this hash swap) is a stepping stone that eventually deletes
   itself once the IOSurface path lands, and names the upstream
   API gap in `servo-paint-api` that blocks step 2.

cargo test --bin ely_app: 120 passed, 0 failed, 2 ignored.
Hash collision probability remains ~1 in 2^64; AHash uses the same
keyspace as the previous SipHash13.
2026-05-10 20:25:01 -04:00