Backtrack explores Atlas-like back navigation across tabs in Brave and other Chromium browsers on macOS.
When a link opens a child tab, the intended final behavior is:
Child tab has meaningful internal back history
→ navigate back inside the child tab first
Child tab is back at its original entry point
→ close the child tab and focus its opener
False positives are considered much worse than a missed transition. Any unclear state must result in no special action.
Repository documentation, user-visible development text, tests, and GitHub planning are maintained in English.
Phase 2, four bounded components complete.
The Phase 1 gesture proof of concept remains available. A Manifest V3 service
worker now validates whether a newly opened tab has a still-existing,
unambiguous opener in the same browser window. It prefers Chromium's
openerTabId and, since version 0.5.1, supplements a missing value only when
the browser's webNavigation.onCreatedNavigationTarget event provides the
exact source-tab and child-tab IDs. Backtrack also
distinguishes meaningful internal history from the captured child-tab entry
point across full-document navigation and single-page applications that use
history.pushState() or history.replaceState(). A guarded action layer can
now activate a freshly revalidated opener and close its child tab at the
tracked entry point.
Version 0.5.0 added the first conservative physical-gesture orchestration.
Automatic actions are disabled by default and remain disabled until the local
back direction is explicitly calibrated. Version 0.5.2 no longer waits for
the full macOS momentum tail: a stronger early policy must remain eligible for
90 ms before it requests the guarded history or opener action. Ambiguous,
vertical, or page-owned horizontal movement still fails closed. A window-wide
cooldown prevents the remainder of the same physical movement from acting in
the newly active document or opener tab.
Version 0.6.0 adds local visual feedback without changing that action gate.
Once a calibrated back movement is already clear, a small arrow appears in the
middle of the page and its ring follows gesture progress. It becomes blue when
the stronger early-action threshold is armed, then moves toward the back
direction and fades on commit.
Rejected or incomplete movement simply fades the arrow away. The indicator is
an isolated, non-interactive overlay: it neither reads page content nor delays
the guarded navigation request, and reduced-motion preferences are respected.
Version 0.6.1 restores ordinary back navigation in tabs without a safe opener
or a tracked child entry. Previously, root overscroll containment suppressed
native Back there, but the action layer supplied no replacement. A confirmed
gesture now requests normal browser history traversal in the freshly checked
active tab unless the tab qualifies for the separately guarded opener action.
Missing closure evidence never authorizes a close. Momentum, in-progress
navigation, inactive senders, and action failures still block further action.
Version 0.6.2 leaves normal navigation in verified root tabs to Brave itself.
When neither openerTabId nor the browser's exact navigation-target fallback
provides an opener, Backtrack restores the page's original overscroll style
and does not classify or animate its wheel input. These tabs no longer pass
through Backtrack's 1.8-second action gate. Child tabs retain the existing
gesture, history, and closure guards. Ownership changes wait until any active
Backtrack sequence and action have ended. This is a bounded root-tab fix, not
a claim that rapid consecutive gestures in nested child tabs are solved.
Real trackpad measurements produced a Conditional Go for a bounded Phase 2 prototype:
- DOM
preventDefault()did not stop Brave's native back gesture. overscroll-behavior-x: containon the root element did stop it in the controlled test.- Slow and fast vertical scrolling produced no horizontal candidate.
- The local horizontal scroll area remained usable and was correctly blocked as a navigation candidate.
- Real tables, carousels, Kanban boards, and complex web applications still belong to the open extended compatibility matrix.
See gesture-research.md for the evidence and the exact Phase 1 decision.
manifest.json
package.json
src/
├── background/
│ ├── back-decision.js
│ ├── gesture-action-gate.js
│ ├── navigation-message-handler.js
│ ├── navigation-target-handler.js
│ ├── navigation-tracker.js
│ ├── opener-message-handler.js
│ ├── opener-resolver.js
│ ├── service-worker.js
│ └── tab-action.js
├── content/
│ ├── gesture-debug.js
│ ├── gesture-indicator.js
│ └── navigation-state.js
└── shared/
├── gesture-commit-policy.js
├── gesture-classifier.js
├── gesture-visual-policy.js
├── messages.js
└── navigation-snapshot.js
docs/
├── gesture-fixture.html
├── indicator-fixture.html
├── gesture-research.md
├── gesture-safety.md
├── internal-history.md
├── navigation-fixture.html
├── opener-safety.md
└── tab-action.md
tests/
├── back-decision.test.js
├── back-navigation-regression.test.js
├── gesture-action-gate.test.js
├── gesture-classifier.test.js
├── gesture-commit-policy.test.js
├── gesture-visual-policy.test.js
├── navigation-message-handler.test.js
├── navigation-target-handler.test.js
├── navigation-snapshot.test.js
├── navigation-tracker.test.js
├── native-root-back.test.js
├── opener-message-handler.test.js
├── opener-resolver.test.js
└── tab-action.test.js
There is deliberately no build step and no external dependency. Brave can
load this directory directly as an unpacked extension. package.json contains
only the local test command.
- Open
brave://extensions. - Enable Developer mode in the upper-right corner.
- Select Load unpacked.
- Select the repository directory.
- Reload any test pages that were already open.
Local repository path used during development:
/Users/bodhi/Documents/Codex/Backtrack
The extension is also expected to load in Google Chrome and other Chromium browsers, but Brave on macOS is the primary target.
src/content/gesture-debug.js observes horizontal wheel sequences at
document_start. It records normalized deltas, axis dominance, cancelability,
scroll context, preliminary thresholds, and sequence boundaries. The pure
classifier rejects vertical movement, short or inconsistent input, synthetic
events, modifiers, non-pixel wheel input, page-canceled events, and horizontal
scroll areas that may own the interaction.
If and only if the direction was calibrated and automatic actions were
enabled and the tab is not a verified root, the content layer applies root
overscroll containment, confirms that the CSS took effect, and sends one
semantic BACK_GESTURE request. It can send
that request before the diagnostic sequence ends only after the stronger
early-commit thresholds remain valid for 90 ms. A session-only background gate
enforces one action per gesture ID and a 1.8-second cooldown across the whole
window against split or retargeted momentum tails. See
gesture-safety.md.
In a verified root tab, Brave owns ordinary Back, Forward, and momentum
handling, including its own native feedback. Backtrack shows its custom arrow
only on the extension-controlled path. BacktrackGestureDebug.getStatus()
reports navigationOwner: "BROWSER" or "BACKTRACK" for local diagnosis.
src/shared/gesture-visual-policy.js permits feedback earlier than an action,
but reuses all direction, page-ownership, scroll-area, modifier, trust, and
calibration blockers. The indicator begins at 80 horizontal pixels and never
performs navigation itself. src/content/gesture-indicator.js renders only a
fixed, pointer-transparent overlay inside a closed Shadow DOM, so site styles
cannot normally alter it and it cannot intercept clicks or scrolling.
The ring shows progress toward the stricter 720-pixel early-commit distance. The action thresholds and 90 ms confirmation remain unchanged. If the gesture does not reach an action, the arrow fades out without changing the page. The visual itself stays centered in the viewport so it is not missed at the edge.
The background process prefers openerTabId without requesting the broad
tabs permission. Some link-created tabs do not expose that property. For
those tabs only, Backtrack accepts Chromium's dedicated
onCreatedNavigationTarget event as a session-only source-to-child mapping.
It rejects missing, conflicting, closed, moved, discarded, pinned-child, or
cross-context relationships. The same exact relationship is validated again
immediately before activation and immediately before child-tab closure.
See opener-safety.md.
The content script reads opaque NavigationHistoryEntry.key values from the
Navigation API. The background process remembers the captured child entry key
and compares it with the current entry key:
current key differs from child entry key
→ USE_INTERNAL_HISTORY
current key equals child entry key and opener is still safe
→ RETURN_TO_OPENER_ELIGIBLE
missing or contradictory evidence
→ NO_SPECIAL_ACTION
history.length is logged for diagnostics only and is never used as the sole
decision signal. See internal-history.md.
These are child-closure decisions, not permission to disable ordinary Back.
When closure is ineligible, the action layer can return USE_BROWSER_HISTORY:
the content script requests history.back() without assuming that the
Navigation API exposes every earlier entry. At the start of browser history,
that request does nothing and the tab stays open.
src/background/tab-action.js consumes only a confirmed semantic back request.
It takes control only when the decision layer reports the exact tracked entry
point and a live, same-window opener. It activates the opener first, validates
the relationship once more, and only then closes the child. If closing fails,
it attempts to restore focus to the still-open child.
The gesture layer can call this action only after either the stronger confirmed early classification or the completed-sequence fallback succeeds. See tab-action.md.
- Open an ordinary
https://page. - Open DevTools (
⌥⌘I). - Enable Preserve log.
- Enable the Verbose log level. Individual events use
console.debug; sequence start, end, and threshold crossings are also highlighted. - Filter for
[Backtrack:Gesture]or[Backtrack:Navigation].
For real navigation tests, close DevTools before performing the physical gesture. In the tested Brave version, DevTools docked on the right prevented native two-finger back navigation by itself. DevTools is useful for capturing events but cannot alone prove that Backtrack suppressed browser navigation.
For an intermittent missed close, Backtrack also keeps the most recent 160 meaningful gesture and action decisions in an on-device diagnostic ring. It survives a page, tab, extension-service-worker, or browser restart, and is overwritten from oldest to newest once it is full. This is a development aid, not telemetry: nothing is sent anywhere.
On any ordinary http:// or https:// page, choose Backtrack Development
in DevTools' JavaScript context and run:
await BacktrackGestureDebug.getPersistentDiagnosticLog()The important sequence is normally one GESTURE_SESSION followed by a
BACK_ACTION. The session shows whether the movement became an action
candidate (and which safety blocker stopped it); the action shows the resolved
decision, for example RETURNED_TO_OPENER, USE_INTERNAL_HISTORY, or
NO_SPECIAL_ACTION with its exact reason. GESTURE_OWNERSHIP explains whether
the current tab was intentionally left to Brave's normal navigation.
Clear the ring after we have inspected an incident:
await BacktrackGestureDebug.clearPersistentDiagnosticLog()The ring keeps only a whitelisted, compact diagnostic schema: numeric tab and window IDs; gesture classification, direction, and rounded threshold values; and action/decision reason codes. It rejects URLs, page titles, page text, raw wheel events, arbitrary page data, and browser history. Ordinary vertical scrolling is not written to the persistent ring.
- Open
brave://extensions. - Select the service-worker link for Backtrack Development.
- Open a link from an existing tab in a new tab.
- Filter for
[Backtrack:Opener].
An ok: true result confirms only the current opener relationship. It performs
no action. The diagnostic object contains no URL, title, favicon, or page
content.
On an ordinary page, choose Backtrack Development from the JavaScript context menu in DevTools, then run:
BacktrackNavigationState.requestBackDecision()Possible results:
USE_INTERNAL_HISTORY: the child still has an internal back step;RETURN_TO_OPENER_ELIGIBLE: the child is back at its captured entry point and its opener is still safe;NO_SPECIAL_ACTION: evidence is missing or contradictory, or the opener is no longer safe.
This method remains diagnostic-only.
The manual Brave 152.1.94.117 smoke test on August 30, 2026 covered:
child entry
→ two SPA push steps
→ one back step, still internal
→ second back step, entry reached
→ full-document navigation
→ tab without opener
The decision changed from USE_INTERNAL_HISTORY to
RETURN_TO_OPENER_ELIGIBLE only when the real child entry was reached.
For a controlled smoke test, open a fresh child tab from an
ordinary http:// or https:// page. In the child tab's isolated Backtrack
Development DevTools context, run:
BacktrackNavigationState.performConfirmedBackAction()This command can close the current child tab. It returns one of:
RETURNED_TO_OPENER: the opener was activated and the child was closed;USE_INTERNAL_HISTORY: the child still has internal back history, so no tab action occurred;USE_BROWSER_HISTORY: no safe child-close decision; ordinary browser Back is available to automatic gesture orchestration;NO_SPECIAL_ACTION: the sender is no longer eligible, navigation is in progress, or an API step failed.
The manual development command only reports either history result; unlike an
automatic gesture request, it does not call history.back().
This development command bypasses gesture classification but not the opener or
history safety checks. The provisional threshold-crossed signal never calls
it.
The successful controlled run used Brave 152.1.94.117 on macOS 26.6.2: a
fresh fixture child closed and its exact opener became the visibly selected
tab. See tab-action.md for the action order and failure
behavior.
Each structured object has a kind field:
wheel: one raw and normalizedwheelevent;session-start: start of one related event sequence;threshold-crossed: the preliminary base threshold, never an action by itself;early-commit-armed: the stronger fast-path threshold became eligible;early-commit-disarmed: a safety condition changed during confirmation;gesture-committed: the stronger evidence remained eligible for 90 ms and an action request is about to be considered;gesture-indicator-shown: the safe visual-only preview threshold was met;gesture-indicator-phase: the stronger action threshold became armed;gesture-indicator-hidden: later evidence invalidated the visual preview;session-end: summary and conservative classification;post-dispatch-default-prevented: the page probably canceled the event after Backtrack's capture listener.
Completed measurements are also emitted as one compact
[Backtrack:Gesture:SessionJSON] line. Threshold crossings are preserved as
[Backtrack:Gesture:ThresholdJSON], keeping the last scroll context visible
with Preserve log even if Brave destroys the old page during navigation.
POSITIVE_X and NEGATIVE_X do not inherently mean back or forward. The
mapping depends on hardware, macOS settings, and browser behavior. Backtrack
stores the explicitly calibrated mapping locally.
The content script runs in an isolated JavaScript world. Choose Backtrack Gesture Research or Backtrack Development from the DevTools context menu before using these commands.
Show status:
BacktrackGestureDebug.getStatus()Preview the visual states without performing navigation:
BacktrackGestureDebug.previewIndicator(0.55, "tracking")
BacktrackGestureDebug.previewIndicator(1, "armed")
BacktrackGestureDebug.hideIndicator()These development commands only display or hide the overlay. They neither simulate a trusted trackpad event nor request any history or tab action.
Enable automatic actions only after observing which sign the normal physical back swipe produces on this Mac:
await BacktrackGestureDebug.calibrateBackDirection("NEGATIVE_X")
// or "POSITIVE_X" on a configuration that reports the opposite signDisable actions while keeping the measured direction:
await BacktrackGestureDebug.disableAutomaticActions()Remove the calibration completely:
await BacktrackGestureDebug.clearCalibration()Calibration stores only the direction and enabled/disabled state in local
extension storage. The separate, bounded diagnostic ring above stores no
address, page content, raw wheel events, or browser history. Automatic actions require computed root
overscroll-behavior-x: contain; a failed containment check becomes a no-op.
Clear the measurement buffer:
BacktrackGestureDebug.clearLog()Finish and summarize the current sequence:
BacktrackGestureDebug.finishSession()Export measurements as JSON:
copy(BacktrackGestureDebug.exportJson())Raw gesture measurements remain only in the memory of the current page frame. Reloading or closing the page removes them. They are never transmitted or stored persistently. The direction calibration and the separate compact diagnostic ring described above survive a reload.
The PoC observes only by default:
BacktrackGestureDebug.getConfig().preventDefaultMode
// "off"Temporarily cancel horizontally dominant events for test case D:
BacktrackGestureDebug.configure({ preventDefaultMode: "horizontal" })Disable the experiment afterwards:
BacktrackGestureDebug.configure({ preventDefaultMode: "off" })The horizontal mode may interfere with horizontal scrolling. The stronger
all mode cancels every cancelable wheel event and must not remain active
during ordinary browsing.
Test root overscroll containment separately:
BacktrackGestureDebug.setRootOverscrollBehavior("contain")
BacktrackGestureDebug.setRootOverscrollBehavior("unchanged")This is also a research switch only. unchanged restores the previous inline
value.
Before every case, clear the buffer, perform exactly one gesture, wait briefly, and save the JSON export. Record the browser version, macOS version, trackpad model, and the Natural scrolling setting.
Start the local fixture server from the repository root:
python3 -m http.server 8765 --bind 127.0.0.1Open:
http://127.0.0.1:8765/docs/gesture-fixture.html
The page loads no external resources. Its blue horizontal area starts in a middle position so both directions can be tested.
- Use a simple ordinary page.
- Swipe right once, then left in a separate measurement.
- Record sign, event count, total distance, and native browser behavior.
- Scroll up and down normally several times.
- Expected:
session-end.evaluation.classificationremainsNO_CANDIDATE, usually because horizontal dominance is too low.
- Swipe over a large table, carousel, or horizontal code area.
- Check whether
horizontalScrollContextis detected. - The PoC blocks a candidate whenever a detectable horizontal scroll area is involved, even if that area is currently at an edge.
- Navigate to a second page in the same tab.
- Perform native back with
preventDefaultModeoff. - Compare the visible
wheelstream with actual browser navigation. - Repeat with
preventDefaultMode: "horizontal". - Optionally run a separate comparison with root containment enabled.
- Reset every research switch after the test.
- Perform a short fast movement and lift both fingers.
- Inspect how many decaying events follow.
DECAY_TAIL_ONLYis explicitly a heuristic. StandardWheelEventexposes no reliable momentum phase.
The complete evidence matrix is in
gesture-research.md. Automated suites through
version 0.6.0 and calibrated physical end-to-end results are recorded separately in
regression-matrix.md.
The completed diagnostic sequence is classified as a horizontal candidate when:
- net horizontal distance is at least 240 CSS pixels;
- accumulated horizontal movement is at least 4 times vertical movement;
- at least 90% of horizontal movement keeps the same direction;
- at least 8 pixel-mode events are present and one reaches 8 horizontal pixels;
- no horizontal scroll area can consume the movement;
- an inner horizontal scroll area is rejected even at its boundary;
- no modifier key is pressed;
- the page did not observably cancel the default behavior itself.
threshold-crossed remains a provisional research log and never requests an
action by itself. Version 0.6.0 may show the non-interactive arrow after 80
horizontal pixels, 3:1 dominance, 85% direction consistency, four pixel-mode
events, and a 6-pixel peak, but only when every ordinary safety blocker passes.
Visual eligibility is not action eligibility. To avoid waiting several seconds
for macOS momentum, version 0.5.2 added a stricter early action path: at least
720 horizontal pixels, 5:1
dominance, 95% direction consistency, 12 pixel events, a 12-pixel peak, every
normal safety check, and a further 90 ms confirmation period. If that path is
not conclusive, Backtrack retains the completed-sequence fallback. The policy
and its conservative tradeoffs are documented in
gesture-safety.md.
| Access | Why needed? | Can it be avoided? | Theoretical data access |
|---|---|---|---|
storage |
chrome.storage.session keeps opaque child-entry and short gesture-cooldown state across service-worker suspension. chrome.storage.local keeps the user's explicit direction calibration and enabled/disabled choice plus a bounded, local diagnostic ring of 160 safe summaries. |
Not safely for the current design. Losing an entry baseline or momentum claim must fail closed, the chosen direction must survive page reloads, and an intermittent issue needs evidence across tab closure. | The permission could also store arbitrary extension data. Backtrack stores only the documented diagnostic schema: numeric tab/window IDs, rounded gesture-threshold values, classification, and action/decision codes. It stores no URLs, titles, page content, raw wheel events, or browsing history. |
webNavigation |
onCreatedNavigationTarget supplies the exact source-tab and child-tab IDs when Brave omits openerTabId for a link-created tab. Backtrack ignores the event URL and keeps only the two numeric IDs in session memory. |
Avoiding it caused real link-created tabs to fail with NO_OPENER. Inferring the source from the active tab or tab position would be unsafe. |
The API can theoretically expose navigation events and their URLs. Backtrack subscribes only to the new-target event, does not log or store its URL, and contacts no server. |
No tabs permission |
The background uses tab lifecycle events plus chrome.tabs.get(), chrome.tabs.update(), and chrome.tabs.remove() for IDs, state validation, activation, and exact child closure. These operations do not require the broad permission. |
Already avoided. | Without tabs, the API does not expose privileged URL, title, or favicon fields to Backtrack. |
Automatic content script on http://*/* and https://*/* |
Gesture and history changes must be observed early across ordinary websites. | An activeTab research build is possible but would require a toolbar action, service worker, and an extra step on every page. Reassess before production. |
A content script could theoretically read or alter page DOM. Backtrack processes only event, geometry, scroll-context, and opaque navigation-entry data. It logs no URL and contacts no server. |
Backtrack does not run on brave://, chrome://, the Chrome Web Store, or
other protected browser pages. This also includes Chromium's internal
chrome-error://chromewebdata/ document: when an https:// navigation fails
because of TLS, DNS, or another network error, the address bar may still show
the requested site while the actual document is a protected browser error
page. Backtrack cannot receive trackpad events there. file:// is not matched.
Subframes are included only when their own address matches http:// or
https://.
- Web content does not necessarily receive the same information as Brave's native macOS gesture machinery.
- Standard
WheelEventcannot reliably identify trackpad versus mouse and exposes no standardized gesture or momentum phase. - Browser-generated network and certificate error pages are protected pages; the ordinary two-finger gesture cannot be detected there by a content script.
- Sites with custom JavaScript gesture logic may look like ordinary scroll areas or evade DOM scroll detection entirely.
- DevTools Preserve log is still useful for the raw per-page research log. The separate persistent diagnostic ring is available after real navigation and tab closure.
- Measurement buffers in embedded frames are separate.
- Direction calibration currently uses the isolated development API; there is no user-facing calibration screen yet.
- Automatic behavior is intentionally off until calibration succeeds.
- Inner horizontal scroll areas are blocked even at their edge, which prefers a missed back action over an accidental tab closure.
- Tabs open before the extension is loaded or reloaded receive no invented entry point.
- Tabs created by browser UI, extensions, restored sessions, or other paths
that provide neither
openerTabIdnor an exact navigation-target event stay open. - Browser or extension restart clears volatile history state; affected tabs are never automatically closed. Ordinary Back remains available after the current content scripts have loaded; refresh pages open before an update.
- Protected pages provide no content-script history evidence and therefore trigger no special action.
- Chrome: content scripts
- Chrome: Tabs API
- Chrome: Web Navigation API
- Chrome: Navigation API
- WHATWG: Navigation API
- Chrome: Storage API
- Chrome: extension service workers
- W3C UI Events
- Chromium: macOS HistorySwiper
- Chromium: OverscrollController
- Chrome: overscroll behavior
- automatic tab action before explicit direction calibration;
- a persistently stored tab tree or browser history;
- an options page;
- telemetry, server access, a persistent tab tree, or browsing-history storage.