Browser URL Suggestions — Delivery Notes
Delivery summary for URL-bar history suggestions. For the decision record — global-vs-per-window scope, the frecency formula, the DuckDuckGo switch — read ADR-0021: URL-bar history suggestions; this note says what shipped and where.
Status
Implemented in tranquil-browser only; manually verified 2026-08-04. Not yet committed.
| Piece | State |
|---|---|
lib/history-suggestions.js — index, frecency scoring, typed store, DDG fetch, row renderer | ✅ built |
| URL-bar source rewired: history → favorites → DuckDuckGo, two-phase, deduped | ✅ built |
Typed-URL recording (Enter navigations + suggestion picks, bp.typedUrls) | ✅ built |
| Custom dropdown rows (icon + title + dimmed URL, theme-agnostic) | ✅ built |
| HAR-replay tabs excluded from history + typed recording | ✅ built |
Bing osjson.aspx removed | ✅ gone |
What shipped
Typing two or more characters in a browser tab’s URL bar now suggests previously visited pages — matched case-insensitively against both the URL (scheme/www.-stripped) and the page title —
ranked by frecency (visit count weighted by recency buckets: today ×100, ≤7 d ×70, ≤30 d ×50,
older ×30; 90-day scan horizon) with an additive boost (400 + 200·min(n, 8)) for URLs the user
explicitly typed or picked. History rows lead the dropdown (clock icon, title, dimmed URL), then
matching favorites (star), then DuckDuckGo search completions (magnifier) filling to ten, all
deduplicated by normalized destination so http/https/www. variants collapse to one row.
History is aggregated across every window’s store (bp.history.<windowSessionId>): per-window
session isolation keeps cookie/login jars apart, not browsing recall — the Firefox-containers
trade-off. A brand-new isolated window gets useful suggestions from the first keystroke.
Search completions moved from the legacy plain-http Bing endpoint (undocumented, no error handler,
silent failures) to https://ac.duckduckgo.com/ac/?type=list — the same endpoint and [term, [completions]] shape the start page already uses — fetched over Node https in the host
renderer, always resolving (offline just means history/favorites only).
Where everything lives
All in tranquil-browser; no core, theme, or third-party changes.
lib/history-suggestions.js (new)
normalizeUrl/isSearchUrl/isSuggestibleUri— canonical scheme-less form for matching, dedupe, and display; DDG-navigation detection;https?/file+ length gate (keepsblob:,about:, giantdata:URIs out of suggestions).buildIndex/getIndex/invalidate— walks everybp.history.*key newest-first with early-exit at 90 days / 5 000 entries per key, merges per URI (visit weights, last-day, title frombp.title, typed record), 30 s in-module cache.historySuggestions— substring match + frecency sort + variant collapse.localSuggestions— phase 1: history rows, then favorites (full{label, value}objects now).appendSearchSuggestions— phase 2: DDG completions deduped against phase 1 by destination.recordTypedUrl— upsertsbp.typedUrls({norm: {n, last}}, 200-entry cap, oldest-by-use eviction) via the same mutate-then-setpatternaddHistoryuses; no-ops for searches and internal pages.fetchSearchSuggestions— host-renderer twin of the preload’ssuggest(Nodehttps, 4 s timeout, always resolves).renderItem— the jquery-ui_renderItembody:<li><a class="tb-suggestion …">with inline-SVG icon,.text()-inserted title (titles are untrusted page content), dimmed normalized URL.
lib/tranquil-browser-view.js
- Autocomplete source (in
initialize) — two-phase: synchronouslocalSuggestionsresponse, thenfetchSearchSuggestions().then(…)re-responds with completions appended. The BingjQ.ajaxblock is deleted. selecthandler — records the pick as a typed navigation, thengoToUrlas before.- Attach block — installs
renderItemon the widget instance (base1.data("ui-autocomplete")), and invalidates the index on URL-bar focus so each session starts fresh against other windows’ writes. recordTypedNavigation(new method, nearsuggestionTarget) — HAR-replay guard +recordTypedUrl.- Enter handler — records the final resolved URL as typed just before navigating (search values filter themselves out downstream).
addHistory— now returns early for HAR-replay tabs (model.opt.isHarReplay): replaying an archive isn’t browsing. Side effect: replay visits also stop appearing on the history page — intentional; move the guard if archival listings are ever wanted.
styles/tranquil-browser.less
- Existing
.ui-autocompleteblock gainsmax-width: 620px; overflow: hidden(jquery-ui’s_resizeMenusets an inline width; the cap makes long titles ellipsize instead of growing the menu). - New global
.tb-suggestion*rules — flex rows, ellipsizing title/URL spans,opacity-dimmed secondary text and icons. Deliberately color-free: the menu attaches to<body>(outside the theme-scoped element), and the business/classic themes already restylebody .ui-autocompleteglobally, so rows inherit each theme’s anchor color untouched.
Gotchas worth remembering
- History
datefields are locale strings, not timestamps (new Date().toString()inaddHistory). All recency math uses the"YYYYMMDD"day-bucket keys; never parse entry dates. - jquery-ui 1.10’s
_normalizeshort-circuits on the first item: ifitems[0]haslabel+value, the whole array passes through un-normalized. History objects lead the array now, so every suggestion in everyres()call must be a full object — favorites were bare strings before and only worked by coming first. - The two-phase
res()pattern is legitimate: the widget’s_responseproxy re-renders per call and itsrequestIndexguard drops replies for superseded terms — no stale-response handling needed in our code. (Each extra call decrements the widget’spendingcounter below zero; harmless, only feeds an unstyled loading class.) .data("ui-autocomplete")must go through the widget’s own jQuery (jQ = require("jquery")), not space-pen’s$— separate jQuery copies keep separate data expandos, and the wrong one returnsundefinedsilently. 1.10 has no.autocomplete("instance").- Menu rows must be
<li><a>…</a></li>in 1.10 — an anchor-less<li>is treated as a divider (unfocusable).menu.refreshaddsui-menu-item/ui-corner-allclasses itself. - jStorage
get()returns live references into its in-memory cache: the index walk is strictly read-only, and writers followaddHistory’s mutate-then-setidiom. Cross-window freshness is automatic (jStorage’sstorage-event observer), so the 30 s cache + focus invalidation exist purely to avoid re-aggregating per keystroke. - Dropdown icons are inline SVGs, not
<img src>: the menu lives on<body>outside the theme scope, andcurrentColoronly follows the themed anchor color when the SVG is inline. History/favorite icons are read from the sameresources/icons/*.svgfiles the toolbar uses; the magnifier matches the find bar’s. - Enter and row-selection can’t double-fire: with a menu row active, the widget’s ENTER
keydown
preventDefault()s, which suppresses the input’skeypresshandler — so recording typed URLs in both paths counts each navigation exactly once.
Manual verification checklist
- Type 2+ chars of a visited site → history rows first (clock, title, dimmed URL), then favorites, then DDG completions filling in a beat later.
- Arrows fill the input with the destination URL; Enter on a highlighted row navigates once; Esc restores the typed text; mouse click navigates.
example.com+ Enter, then typeexam→ it outranks equally-visited pages;window.bp.js.get('bp.typedUrls')shows it; a plain search (foo bar+ Enter) adds nothing.- Second window: visit a unique site there → first window suggests it after re-focusing its URL bar (cross-window aggregation).
- Delete a day/entry on
tranquil-browser://history→ gone from suggestions after refocus. - Offline: dropdown still shows history/favorites; no console errors.
- Blank tab: bar empty; nothing
tranquil-browser://is suggested or recorded. - Dropdown legible in business dark + light (rows inherit the themes’ menu colors).
- Open a
.hararchive: replay navigations no longer append to the history page.