Skip to content

Desktop: browser-style back/forward navigation history for the center pane #52

Description

@samkeen

Browser-style back / forward navigation history for the note pane (the center display), so the user can retrace their reading path. Clicking a similar-note card, a backlink, a wikilink, a tree entry, or a search result all move the center document forward; a Back control returns to the previous one, Forward re-advances. Pure frontend — no façade, host, or b2-core change (every target is already read through the existing read_note / explain_resource commands).

Why

Discovery is the whole point of B2 — you follow "similar & unlinked" and backlinks from note to note. Today every hop is one-way: there's no way to step back to where you came from without re-finding it in the tree or search. Browser-grade back/forward makes exploratory reading fluid.

Behavior spec

  • What a history entry is: the document shown in the center pane, i.e. an openNote or openResource target — { kind: "note" | "resource", path }. History tracks the pane's document regardless of how it was reached (tree click, wikilink, backlink, similar card, search result, live-preview link).
  • Push on genuine navigation only. Every real navigation pushes an entry and truncates any forward entries (the browser model: navigating after going back discards the forward branch). Suppress consecutive duplicates (re-opening the already-current document is a no-op for history).
  • Do not push on in-place content updates. Re-reading the same note after a save, applying a write report, or reconciling an external on-disk change all mutate state.current/state.currentResource for the same document — these must not create history entries (ui/src/main.ts ~608, ~869, ~892, ~985, ~1005).
  • Back / Forward reuse the load path without pushing. They move a cursor within the stack and load the entry there, going through the same edit-mode guard as normal navigation: flush + leave edit mode first via closeEditor(), and abort (keep the editor + buffer) on a write conflict — identical to how openNote/openResource already behave.
  • Vault switch clears history. switchVault already resets the center document and side pane; the stack (and cursor) reset with it — old vault paths are meaningless in the new one.
  • Dead targets: if a back/forward target has since been deleted or renamed, the read fails → generic actionable toast (repo error policy); drop that entry from the stack so navigation isn't wedged on a dead node.
  • Session-scoped, in-memory. History starts empty on launch and is not persisted (browser-tab semantics). Persisting across restarts and per-document scroll restoration are explicit non-goals for this slice (possible follow-ups).

UI

  • Two chrome buttons — ◀ Back / ▶ Forward — disabled at the ends of the stack, styled like the existing topbar icon buttons (.btn.icon-btn, e.g. the vault switcher / settings gear). Browser-convention placement is top-left (near the B2 brand); the note-pane bar is an alternative.
  • Keyboard: ⌘[ / ⌘] (macOS browser convention; also accept ⌘← / ⌘→), wired alongside the existing shortcuts in the keydown handler (⌘, / Esc / ⌘S). Mouse back/forward buttons (auxclick) are a nice-to-have.

Implementation sketch (grounded)

There is already a single choke point: every center-pane change goes through openNote(ref) (ui/src/main.ts:135) or openResource(path) (:174) — reached from .wikilink (:1100/:653), [data-open] (:1161), and [data-open-resource] (:1147). ([data-open-system] at :1154 opens a file in the OS default app — not a center-pane nav — and is excluded.)

Suggested shape:

  1. Extract the "load this document into the pane" core out of openNote/openResource (everything after the closeEditor() guard). Back/forward call the core directly.
  2. Keep the wrappers as the user-navigation entry points: guard → push entry (truncate-forward, dedupe) → load core.
  3. Hold the stack + cursor as module-locals in main.ts (like editorView / save-chain flags), and paint the buttons' enabled/disabled state with a targeted paintNav() (the paintReindex / paintEditor pattern) rather than widening AppState.
  4. Optional: cap the stack length (e.g. last ~100 entries) to bound growth.

The history-stack logic (push / truncate-forward / dedupe / back / forward / clear) is pure and unit-testable in isolation if/when a ui/ test harness is added.

Scope

  • In: back/forward over notes and resources; buttons + keyboard; edit-mode flush; vault-switch clear; dead-target pruning.
  • Out (non-goals): cross-restart persistence; per-document scroll restoration; multiple tabs/panes; a visible history dropdown.

Ref: ui/src/main.ts (openNote / openResource choke point, the keydown handler, switchVault); planning/specs/completed/desktop-ui-mvp.md (thin-adapter / one-document-owns-the-pane); crates/b2-desktop/CLAUDE.md (why this stays a pure ui/ change).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions