Docs

ADR-0021: URL-bar history suggestions

Status: Active · Date: 2026-08-04

References

Context

Typing in a browser tab’s URL bar pops a jQuery-UI autocomplete[^1] fed by two sources: locally saved favorites (bp.fav) and Bing’s legacy OpenSearch endpoint (http://api.bing.com/osjson.aspx — undocumented, plain http, and with no error handler, so when it fails it fails silently). What it never suggested was the user’s own browsing: pages already visited, addresses already typed.

The data was already there. Every navigation is recorded by addHistory into a per-window jStorage key — bp.history.<windowSessionId> — as day-buckets of {date, uri} entries, with page titles in a parallel bp.title map. These stores back the tranquil-browser://history page, but the URL-bar autocomplete never read them.

The wrinkle is per-window session isolation: every window is its own persisted cookie/login jar (persist:tb-window-<id>), so “which history feeds which window’s suggestions?” is a real product question, not just plumbing. Two facts shape the answer. First, the visit logs live in the host renderer’s localStorage, which all windows share — isolation partitions the guest webviews, not the host — so aggregation is technically trivial. Second, there is precedent: Firefox Multi-Account Containers separates cookies and site storage per container while history and address-bar suggestions stay shared[^2] — isolation protects login state, not recall.

Decision

Suggest previously visited pages in the URL bar, ranked by frecency, aggregated across all windows’ history stores — with a boost for URLs the user explicitly typed — above DuckDuckGo search suggestions. All in tranquil-browser; a new lib/history-suggestions.js module owns the logic:

  1. Global history scope. The suggestion index merges every bp.history.* key, joined with bp.title. Window isolation continues to mean “separate logins,” never “separate recall” — a brand-new window suggests URLs typed anywhere, immediately.
  2. Frecency ranking[^3] with a typed boost. Each visit contributes a recency-bucketed weight (today ×100, ≤7 days ×70, ≤30 days ×50, older ×30; buckets older than 90 days are not scanned); matching is case-insensitive substring over the normalized URL + title, with a ×2 multiplier when the term prefixes the URL. URLs the user deliberately navigated to — Enter on a URL-ish value or picking a concrete suggestion row — accrue an additive boost (400 + 200·min(n, 8)) from a new capped bp.typedUrls store, so a once-typed address decisively outranks incidentally visited pages, while a heavily revisited page can still win. Plain searches never count as typed.
  3. One merged dropdown, three row kinds. History rows (clock icon, title + dimmed URL) first, then favorites (star), then search completions (magnifier) filling to ten total, deduplicated by normalized destination so http/https/www. variants collapse and a search row never duplicates a history row.
  4. DuckDuckGo replaces Bing for search completions: the ac.duckduckgo.com/ac/?type=list OpenSearch-suggestions endpoint[^4] the start page already uses, fetched over Node https in the host renderer (CORS-exempt, always resolves — failure degrades to “history and favorites only”).

Options considered

  • Option A: History scope — global across windows (chosen) vs. per-window strict vs. hybrid (global, own-window boosted). Per-window recall would make every new window start amnesiac and would pretend the host-side stores are isolated when they aren’t. The Firefox-containers precedent[^2] matches user expectations: separate logins, shared memory. The hybrid was deferred — the per-window keys are preserved, so an own-window boost is a one-line scoring tweak later if wanted.
  • Option B: Data source — reuse the existing jStorage visit logs (chosen) vs. a new SQLite/StateStore visit database. The day-bucketed jStorage log already captures every visit, syncs across windows via storage events, and is what the history page reads and deletes from — reusing it means deletions instantly stop feeding suggestions, with no migration and no second source of truth. A real database becomes worth it only if history outgrows the 90-day / capped scan this design bounds itself to.
  • Option C: Ranking — frecency buckets + typed boost (chosen) vs. recency-only match vs. Chrome-style learned scoring. Recency-only surfaces whatever was open five minutes ago regardless of relevance; a learned model is machinery this scale doesn’t need. Bucketed frecency is deterministic, explainable, and computable in one linear pass.
  • Option D: Search completions — DuckDuckGo over Node https (chosen) vs. keep Bing vs. drop search suggestions. The Bing endpoint is an undocumented plain-http legacy API that fails silently today; DuckDuckGo is already the browser’s search engine and the start page’s suggestion source, so this also collapses two providers into one. Node https in the host sidesteps CORS the same way the start page’s preload does.

Consequences

  • The URL bar finally recalls where you’ve been — matched on both URL and title, ranked by how often and how recently, with typed addresses first in line. A fresh window is immediately useful.
  • Cross-window visibility is now intentional and documented: any window can surface a URL visited in any other window. If a “private window” mode ever ships, it must opt out at the recording end (addHistory / recordTypedNavigation), not by filtering suggestions.
  • Deleting history works end-to-end for free: the history page edits the same stores the index reads, so removed entries stop being suggested at the next index rebuild (≤30 s TTL, refreshed on every URL-bar focus).
  • HAR-replay tabs no longer pollute history — replaying an archive is not browsing intent, so they’re excluded from both the visit log and the typed store. (Their entries also stop appearing on the history page; if archival visits should be listed there, that guard moves.)
  • Bounded cost, bounded store: scans cap at 90 days / 5 000 entries per window key; the typed store caps at 200 URLs with oldest-by-use eviction; the in-memory index rebuilds lazily.
  • Re-evaluate if history volume outgrows the localStorage-backed log (→ Option B’s database), or if per-window privacy expectations change (→ Option A’s hybrid or a private-window mode).

[^1]: jQuery UI 1.10 Autocomplete — the source(request, response) contract the two-phase merge is built on. https://api.jqueryui.com/1.10/autocomplete/ [^2]: Firefox Multi-Account Containers — containers separate cookies and site storage per container, while history stays shared across them. https://support.mozilla.org/en-US/kb/containers [^3]: Mozilla Places “frecency” — ranking visited pages by a combination of visit frequency and recency-bucketed weights. https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm [^4]: OpenSearch Suggestions extension — the ["term", ["completion", …]] JSON shape DuckDuckGo’s ac endpoint returns. https://github.com/dewitt/opensearch/blob/master/mediawiki/Specifications/OpenSearch/Extensions/Suggestions/1.1/Draft%201.wiki