# Chromium-inspired browsing history (Flow) This document is the **source of truth** for how Flow stores and surfaces browsing history. It is derived from the Chromium History deep dive (URLs + visits, SQLite, per-profile scope, retention) but **kept simpler** than Chromium—no sync, separate Favicons DB, Top Sites, segments, clusters, or downloads-in-history. ## Goals - Persist **real** history for omnibox and **`flow://history`**. - Mirror Chromium’s **core data model**: one row per URL (`urls`) and one row per navigation (`visits`). - Scope data **per profile**, like Chromium’s per-profile `History` file. - Apply a simple **90-day retention** policy aligned with Chromium’s default horizon. ## Non-goals (out of scope) - Parity with Chromium schema v70, transition bitmasks, `from_visit` graphs, or keyword search terms. - Separate `Favicons` / `Top Sites` databases. - Encrypted DB or secure wipe beyond normal SQLite deletes. ## Storage - **Engine:** SQLite via `better-sqlite3` / Drizzle, same `flow.db` as other Flow state. - **Tables:** - `history_urls` — canonical URL row per `(profile_id, url)`: - `id`, `profile_id`, `url`, `title`, `visit_count`, `typed_count`, `last_visit_time` - `history_visits` — one row per recorded navigation: - `id`, `url_id` → `history_urls.id` (**foreign key**, `ON DELETE CASCADE`), `visit_time` (ms since epoch), `typed` (whether this visit was an address-bar / omnibox navigation) **Indices:** unique `(profile_id, url)` on URLs; `url_id` and `visit_time` on visits for queries and pruning. ## When we record a visit - **Triggers:** - Main-frame load completes (`did-finish-load`). - Main-frame **in-page** navigations (`did-navigate-in-page`), e.g. `pushState` / hash changes (SPAs). - **Active tab only:** nothing is written while the tab is **not** the selected tab (or not in the selected tab group) for its window + space — including **session-restored** tabs until you activate them. When a tab **becomes** active, we record the current page once (still subject to consecutive-URL dedupe). `page-title-updated` history title patches apply only while the tab is active. - **URL filter:** only `http:` and `https:` (skip internal `flow:`, `flow-internal:`, `about:`, error pages, etc.). - **Privacy:** do **not** record for **ephemeral** (incognito) profiles. **Ephemeral tabs** (e.g. pinned-tab slot tabs) **are** recorded so pinned browsing appears in history; those tabs still skip **session tab persistence** as before. - **Consecutive same URL (per tab “session”):** while a tab’s `WebContents` is alive, if the **last visit we stored** for that tab has the same **canonical URL key** as the new one, the new one is **ignored** (refresh, duplicate `did-finish-load` / `did-navigate-in-page`, omnibox to the same page, etc.). After you navigate elsewhere and come back, the URL can be recorded again. The key strips the hash; on YouTube, shorts and watch URLs normalize to the video id so tracking query params don’t create false differences. Reset when the tab gets a new `WebContents` (e.g. wake from sleep). - **Title:** use `getTitle()` when non-empty; otherwise fall back to URL hostname. On `page-title-updated` (active tab only), the `history_urls` row for the current URL (same profile + exact URL string) is updated so the stored title tracks the latest document title without adding visits. ### Typed count (`typed_count`) Incremented when the user navigates via the **omnibox / address bar** (submit or choosing a suggestion), including **open in new tab** from the omnibox. Before `loadURL`, main stores the **intended URL string**; only a recorded visit whose committed URL **matches** that string increments `typed_count` (avoids applying the marker after a failed navigation or to a different final URL). Each visit row stores a `typed` boolean so `typed_count` on `history_urls` can be **recomputed** from visits after deletes or retention pruning. - `navigation:go-to` optional `typedFromAddressBar` - `tabs:new-tab` optional `typedFromAddressBar` (initial load only) Other navigations (links, redirects, UI outside the omnibox) do not increment `typed_count`. Navigations that do not produce a history row (non-`http(s)` URL, ephemeral profile, etc.) **clear** any pending typed marker so it does not apply to a later page. ## Surfaces ### Omnibox - **`flow.history.list()`** — aggregated URL rows for the window’s profile. - Ranking uses `visit_count`, `typed_count`, `last_visit_time`, and match quality. ### History page (`flow://history`) - Chronological **visit** list (join visits + URLs), Chromium-style **grouping by calendar day** (Today, Yesterday, …). - **Search** filters title and URL (case-insensitive substring). - **Row actions:** primary row is a normal `` (same-tab navigation); **context menu** (right-click) for open in current tab, new tab, copy link, delete one visit, delete all visits for that URL row. - **Clear browsing data** clears all history for the current profile. - Uses Flow UI patterns (cards, dark theme route, shadcn-style components) like **Extensions**. ### Discoverability - Default **new tab** quick link **History** (`flow://history`). - Omnibox **pedal** for queries like “history” / “browse history”. ## Retention - On database init (after migrations), delete visits with `visit_time` older than **90 days**. - Remove URL rows that no longer have any visits. - Recompute `visit_count`, `typed_count`, and `last_visit_time` on remaining URLs from their visits so aggregates stay consistent. ## API surface (preload → main) All handlers resolve `profile_id` from `event.sender` → browser window → active space (same idea as `profile:get-using`). | API | Purpose | | ---------------------------------------- | ----------------------------------- | | `flow.history.list()` | Aggregated URLs (omnibox) | | `flow.history.listVisits(search?)` | Visit rows for the history page | | `flow.history.deleteVisit(id)` | Remove one visit; reconcile URL row | | `flow.history.deleteAllForUrl(urlRowId)` | Remove URL row and its visits | | `flow.history.clearAll()` | Clear profile history | ## Future extensions - Transition types and “user-visible” filtering like `chrome://history`. - Time-range deletion (not only “clear all”). - Deduping edge cases where `did-finish-load` and `did-navigate-in-page` might both fire for the same logical navigation.