Commit Graph
480 Commits
Author SHA1 Message Date
ZacharyZhang-NY 42dc9af66c perf(sidecar): skip redundant paint barriers 2026-05-16 01:25:37 -04:00
ZacharyZhang-NY 8ad85ce7f7 perf(sidecar): report exact live frame percentiles 2026-05-16 01:16:22 -04:00
ZacharyZhang-NY c6de666d1c test(sidecar): align live perf bench 2026-05-16 01:12:10 -04:00
ZacharyZhang-NY 5f49786c6e refactor(sidecar): split live session state 2026-05-16 01:07:50 -04:00
ZacharyZhang-NY 234bf4c8a5 fix(web-surface): bound live input paints 2026-05-16 01:02:38 -04:00
ZacharyZhang-NY 8841d74ac5 test(web-surface): lock active cadence to 120hz 2026-05-16 00:22:10 -04:00
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 225a998642 fix(tabs): refresh metadata on navigation 2026-05-15 23:31:12 -04:00
ZacharyZhang-NY ca8f118785 perf(web-surface): throttle idle Servo polling 2026-05-15 23:25:09 -04:00
ZacharyZhang-NY 736ff08e90 Fix dead traffic-light hit area under FullSizeContentViewWindowMask
`move_traffic_light` was framing the close / minimize / zoom NSButtons
at `titlebar_height - traffic_light_position.y - button_h`. Once the
window opts into `NSFullSizeContentViewWindowMask`, the content layout
rectangle covers the whole frame and `titlebar_height()` returns 0 —
collapsing the origin to a large negative Y. macOS still painted the
buttons through its own caching layer at the title bar's natural
position, but the buttons' hit-test rectangle followed the frame off
screen, so clicking close / minimize / zoom did nothing.

Anchor the math against the close button's actual superview frame
height (the themeFrame, which equals the window height in Y-up
coordinates) so the visual position and the hit area stay in lockstep
no matter what mask combination the window opens with. `titlebar_height`
stays available for callers that want the legacy non-full-size
interpretation, with `#[allow(dead_code)]` and a docstring pointing at
this fix.
2026-05-15 23:12:43 -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 1c72622db8 Resolve favicons through Google's s2 service
Pointing the renderer at the site's own `/favicon.ico` was producing
GPUI image-cache errors on every other tab — notion.com redirected
across origins, sites shipped multi-image `image/x-icon` blobs the
PNG/WebP decoder couldn't read, hosts 404'd. Each one logged a noisy
`ERROR gpui::asset_cache: Failed to load asset` line.

Switch `UrlText::favicon_url` to
`https://www.google.com/s2/favicons?domain=<host>&sz=64`. Google's
endpoint normalises every response to PNG, follows redirects on its
side, and serves a `_/` globe glyph for sites without a favicon at
all — same URL shape every browser dev-tools panel already shows
for "favicon" so the fetch is uniformly succeeding.
2026-05-15 21:59:30 -04:00
ZacharyZhang-NY 1e480a9f87 Update OTP test to match the new browser@elydora.com sender
The wrangler.toml + better_auth.ts move from auth@ to browser@elydora.com
left this assertion behind, so `npm test` failed pre-deploy. Realign
the expected `from` field and the worker's test suite is green again.
2026-05-15 21:45:29 -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 a47fbdc09c Wire SyncEngine into BrowserCore
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.
2026-05-15 17:10:30 -04:00
ZacharyZhang-NY 2081c6bd88 Update Cargo.lock for ely_sync_client deps 2026-05-15 17:05:14 -04:00
ZacharyZhang-NY 7366a0dffe Add ely_sync_client crate
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.
2026-05-15 17:05:10 -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 9212b0be24 Flip BGRA surface texture sampling 2026-05-13 09:51:59 -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 b91f61c201 Fix live perf bench scroll page URL 2026-05-13 02:59:15 -04:00
ZacharyZhang-NY ac84a80d9e Close Servo live sessions for removed tabs 2026-05-13 02:41:54 -04:00
ZacharyZhang-NY 74731d4faa Keep loading until live content arrives 2026-05-13 02:18:48 -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 8cd5e969dc Guard hardware surface selectors 2026-05-13 00:43:13 -04:00