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.
The design's Slack/Linear/Gmail rows show numeric badges (12, 3) that
real apps publish in their tab title prefix — "(12) Slack | …",
"(3) Inbox — Linear", "(99+) Gmail". Add a pure parser
ely_domain::parse_title_unread_count that recognises the leading
"(N)" pattern and surfaces it through BrowserTab::unread_count(); the
sidebar launcher row renders a glass pill badge whenever the count
is non-zero, capped at "99+" for very high counts.
No domain field, no fabrication: the badge appears only when a real
website publishes its own unread count via the title. Tests cover
canonical formats, non-prefixed titles, leading whitespace, and
non-numeric / overflow inputs.