# Plan: tree-aware Back/Forward (the "lineage cursor") Status: **planned** (not yet built). Goal: navigation that walks the *apparent lineage shown in the panel* — Back goes to a node's tree parent, Forward returns down the branch — instead of the browser's linear session history. ## Why we own a cursor instead of commandeering the physical buttons We audited this (see `src/background/navEntries.ts`). The browser's Back/Forward buttons traverse Chrome's **linear per-tab session-history stack**, and **extensions cannot edit that stack** — the only levers are `tabs.update` (append one entry, truncating forward), `tabs.goBack/goForward` (±1), and same-origin `history.pushState`. There is no "set the history list to this sequence" API. So the two ways to make the *physical* buttons walk the tree are both unviable as a default: - **Replay the lineage as real navigations** (navigate root→…→node so the stack mirrors the path). Works cross-origin, but **loads every intermediate page** — re-firing side effects, unable to replay POST/form navigations, slow, flickery, and silently diverging when a page now redirects/404s/needs auth. It also forces us to suppress our own capture during replay. Fragile. - **`pushState` synthesis** — cheap but same-origin only, and the pushed entries don't render on Back unless the site handles `popstate`. Broken for real sites. Conclusion: stop fighting the platform. Keep `navEntries` as the faithful mirror for when the user presses Chrome's buttons, and add a **tree cursor** that we fully control as the panel's primary, WYSIWYG navigation. ## The model A per-tab **lineage cursor** = a path through the tree plus a position: ``` { path: string[]; // visit ids, root → … → current node index: number; } // where we are along that path ``` - **Check out node N** → `path = ancestors(N) ++ [N]` (walk `parentId` up from N, reverse), `index = path.length - 1`. - **Back** → `index - 1` (the tree parent). - **Forward** → `index + 1` (the child we descended through — this is *why* we store the whole path, not just the current node). - **Retarget to a node M on another branch** → diff `path` against M's lineage, keep the longest common prefix (the **merge base**), replace the suffix with M's. This is the "walk to the merge base, then append the lineage" idea — free and reliable when applied to our own cursor rather than the browser's stack. Structurally this is a sibling of `navEntries`: a small, pure, dependency-free reducer that unit-tests in plain node. ## Two sub-modes (decide which, or offer both) 1. **Highlight-only** — Back/Forward move the *selected/highlighted* node up and down the lineage without navigating the tab. Zero side effects; good for inspecting a branch. Cheap. 2. **Navigating** — each Back/Forward also issues **one** `tabs.update` to that ancestor/descendant URL (the cost of a single link click — fine for a user-initiated step; this is NOT the multi-load replay above). HEAD follows, the page content follows, and the highlight walks the lineage you see. ## Implementation steps 1. **`src/background/treeCursor.ts`** (pure): `LineageCursor` type + `checkout(node, index)`, `back(cursor)`, `forward(cursor)`, `retarget(currentPath, targetLineage)` (merge-base prefix + new suffix). Needs only `parentId` walking — accept an `ancestors(id) => string[]` lookup so the module stays storage-free (same injection trick as `applyBackForward`). 2. **`tests/treeCursor.test.ts`** — straight-line back/forward, forward after back, retarget across a fork (assert the merge base is the common ancestor), single-node and root edge cases. Wire into the `test` script. 3. **Persist** the cursor per tab in `chrome.storage.session` (alongside the `navStack`); clear it in `clearTab`. 4. **Actions** in `src/viewer/views/types.ts`: `lineageBack()` / `lineageForward()` that resolve the next node from the cursor, reuse the existing `openUrl` checkout path to navigate (navigating sub-mode) or just `select` it (highlight-only sub-mode), and advance the persisted cursor. 5. **Panel UI** in `App.tsx`: ▲ parent / ▼ child buttons (disabled at the ends of the lineage), optionally bound to keyboard shortcuts. The highlight already follows HEAD, so it will track the cursor for free. ## Interaction with what exists - **`navEntries` is untouched.** It remains the faithful mirror of the physical Chrome buttons. The tree cursor is a *separate, additional* control; the two coexist (physical buttons = linear/honest, panel buttons = tree-aware). - **Checkout already exists** (`hg:checkout` + `openUrl`); the cursor just wraps it with lineage bookkeeping, so steps 4–5 are mostly glue. - **Documented limitation:** we cannot make Chrome's physical Back/Forward walk the tree. The panel's buttons are the tree-aware path; the physical buttons stay linear. Make that legible in the UI so the two don't feel inconsistent. ## Open questions / decisions before building - Highlight-only, navigating, or both (e.g. a modifier key to inspect vs. go)? - After a **physical** Back/Forward (a `navEntries` move), how do we reconcile the tree cursor — rebuild it from the new HEAD's lineage, or leave it stale until the next checkout? (Rebuilding from HEAD each time is simplest and keeps the two consistent.) - Cross-tab lineage: `openUrl` navigates the *active* tab, and lineages can span tabs (new-tab opens). Decide whether lineage steps that cross a tab boundary switch tabs (`activateTab`) or are disallowed. - Keybindings: do we want Alt+↑/↓ (or similar) for lineage moves, and is that worth the conflict surface?