Navigate the active tab in place instead of spawning a new tab

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.
This commit is contained in:
2026-05-09 22:42:25 -04:00
parent 2852ab63ee
commit f8e05604ed
3 changed files with 61 additions and 1 deletions
+29 -1
View File
@@ -38,6 +38,12 @@ impl ElyShell {
self.open_internal_tab("ely://task-manager", window, cx);
}
/// In-tab navigation. Used by every settings sidebar item, every
/// home pill, and every "go to internal page" affordance — the
/// active tab follows the link instead of spawning a fresh tab
/// for every URL change. Use `open_url` only when the explicit
/// intent is "spawn a new tab" (see `open_new_tab` for the
/// keyboard shortcut path).
pub(super) fn open_internal_tab(
&mut self,
url_text: &str,
@@ -45,10 +51,32 @@ impl ElyShell {
cx: &mut Context<Self>,
) {
if let Ok(url) = UrlText::parse(url_text) {
self.open_url(url, window, cx);
self.navigate_active_tab(url, window, cx);
}
}
/// Navigate the active tab to `url` without creating a new tab.
/// Falls back to opening a new tab only if there's no active tab
/// to navigate (the BrowserCore returns `TabNotFound`).
pub(crate) fn navigate_active_tab(
&mut self,
url: UrlText,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let ShellState::Ready(core) = &mut self.state {
if core.navigate_active_tab(url.clone()).is_err() {
core.open_tab(url);
}
self.sync_address_input(window, cx);
self.focus_address_bar(window, cx);
cx.notify();
}
}
/// Spawn a fresh tab for `url`. Reserved for "+ New Tab" buttons
/// and the deep-link router — anything that explicitly wants a
/// new sibling tab rather than navigating in place.
pub(crate) fn open_url(&mut self, url: UrlText, window: &mut Window, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state {
core.open_tab(url);