The `sed`-based extractions in the preceding three split commits each
left one extra blank line at the cut boundary, which `cargo fmt --all
--check` (a CI gate) rejects. Whitespace only — no code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`scripts/audit_source_lines.sh` (a CI gate) flagged `tests/commands.rs`
(527) and `tests/splits.rs` (524) over the 500-line ceiling. Both are
flat lists of independent `#[test]` fns with no shared helpers, so each
splits cleanly into a sibling integration-test binary (the crate already
uses a topical file-per-concern layout under `tests/`).
- commands.rs (29 tests) -> commands.rs (20: tab/space/profile/search)
+ command_pages.rs (9: internal-page-opening commands). 527 -> 389.
- splits.rs (25 tests) -> splits.rs (13: layout/axis/detach mechanics)
+ saved_split_lifecycle.rs (12: close/archive/restore/group). 524 -> 266.
No tests added or removed; each new file carries only the imports it
uses. `cargo clippy -p ely_browser_core --tests -- -D warnings` clean;
all ely_browser_core tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tabs.rs had grown to 538 lines — past the 500-line ceiling — and was
mixing four unrelated concerns:
- `tab_navigation` (URL changes + history back/forward)
- `tab_metadata` (title, favicon, zoom, favorite/pin, sort, sync flag)
- `tab_archive_restore` (un-archive entry points + query matching)
- core CRUD (open / close / move-to-space + private helpers)
Each split file lives below 250 lines and only pulls the `ely_domain`
types and crate helpers it actually uses. `active_tab_mut` and the
`TabUrlUpdate` enum get `pub(super)` so siblings can reach them; no
behaviour change.
`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`.
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.
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.
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).
`submit_command` matched every browser's "Enter to navigate" intent
to `open_tab(url)`, which always inserts a new `BrowserTab`. So
typing `google.com` and hitting Enter on a new-tab page would leave
both the new-tab and a fresh google.com tab in the sidebar.
Mirror the same in-place navigation rule the shell-level code now
uses: `navigate_active_tab(url)` for the Navigate and Search
intents, with `open_tab` as the fallback when there's no active tab
yet. Tab count only goes up when the user explicitly hits + New Tab.
cargo test --workspace: 440 passed, 0 failed.
Root cause of "settings opens new tab for every click": every
internal navigation went through `open_internal_tab → open_url →
core.open_tab(url)`, and `open_tab` unconditionally inserts a new
`BrowserTab`. So three settings sub-page clicks left four tabs in
the sidebar, which is the screenshot the user keeps sending.
Real browsers navigate the active tab in place for in-app links and
spawn new tabs only on `+ New Tab` (or Cmd-click). Wire it through:
* `BrowserTab::set_url(url)` mutates the tab's URL and bumps
`last_active_at`. Title stays put — the page renderer can refresh
it from the new URL.
* `BrowserCore::navigate_active_tab(url)` finds the active tab,
calls `set_url`, marks it Ready, records the history entry, and
bumps activity. Returns `TabNotFound` if there's no active tab.
* `ElyShell::navigate_active_tab` calls the core method and falls
back to `open_tab` if there's no active tab to navigate. The
shell's `open_internal_tab` (used by settings nav, home pills,
sidebar Settings + Profile rows, command-overlay routes, etc.)
now routes through this in-place path.
* `open_url` keeps the explicit "spawn a new tab" semantics for
`+ New Tab` and the deep-link router.
Settings, plugin marketplace, history, profile picker — every
sidebar nav now stays in one tab.
cargo test --workspace: 440 passed, 0 failed.