How to Build an AI-Powered, Local-First Chrome Extension That Turns Your Browsing History into an Intent Map


Your browser remembers every page you’ve ever opened, but it has no idea why you opened any of them.

You might spend three days comparing laptops across a dozen tabs, get distracted, come back a week later, and your history just shows a flat list of timestamps and titles, with no sense that those visits were one thing, a decision you started and never finished.

In this tutorial, you’ll build openloops, an open-source, local-first Chrome extension that fixes this by scanning your browsing history and grouping it into “intent threads” – the decisions, research, and open questions you keep coming back to – then scoring each one for how alive it still is. Optionally, it also uses Claude to label those threads in plain language, suggest a concrete next step, and power a chat assistant you can ask “what should I close this week?”

By the end, you’ll have built:

  • A Manifest V3 Chrome extension with a service worker and a full-tab dashboard

  • A local pipeline that captures, cleans, segments, and clusters browsing history entirely in IndexedDB

  • A clustering algorithm tuned and debugged on real (messy) browsing data

  • An AI labeling layer using Claude, with a grounding step that uses brand data from context.dev

  • A chat assistant that reasons across your threads and tells you what to do next

  • A polished dashboard with onboarding, a design system, and a working pipeline status machine

Everything runs on-device, and the only network calls are optional and opt-in, made with your own API keys.

Table of Contents

What You’ll Build

On first run, openloops greets you with a centered welcome screen that walks you through the three pipeline steps:

openloops welcome screen, showing the three onboarding steps: scan your history, build sessions, and build your intent map

Once you’ve scanned your history, built sessions, and built the intent map, your browsing reorganizes into status-grouped threads: active, stalled, and dormant. Each one has a confidence score, a plain-language summary, a concrete next step, and a Resume button that reopens the exact pages you left off on. The right column holds a chat assistant grounded in your own threads:

openloops dashboard showing status-grouped intent threads on the left and an AI assistant chat reasoning about what to close this week on the right

That assistant response reasons across the user’s actual threads, ranking them by how easy they are to close against how much of a real decision they still need. It also explains why, which is the most novel part of this build, and depends on the context.dev grounding step you’ll add later in this tutorial.

Prerequisites

To follow along, you’ll need:

  • Node 18+ and a Chromium-based browser (Chrome, Brave, Edge, and so on).

  • Comfort with TypeScript and React. You don’t need to be an expert, but you should be comfortable reading hooks and async/await.

  • Basic familiarity with IndexedDB is helpful but not required, as you’ll learn what you need as you go.

Two parts of this build are optional and require your own API key, each with a free tier:

  • An Anthropic API key (from platform.claude.com) for AI labeling and the chat assistant

  • A context.dev API key (from context.dev) for the brand-grounding step

You can build and use the entire core pipeline, capture, clustering, scoring, without either key, since both are additive layers on top of it.

How openloops Is Structured

Before writing any code, it helps to see the whole shape of the thing. Every stage of openloops reads from one IndexedDB store and writes to the next:

chrome.history (backfill) ──┐
chrome.tabs.onUpdated (live)─┴─→ raw_events
                                     │  noise filter
                                     ▼
                                  sessions
                                     │  ambient detection + clustering + scoring
                                     ▼
                               intent_threads
                                     │
                                     ▼
                              React dashboard
                                     │  optional, opt-in
                                     ├──→ brand enrichment   (context.dev)
                                     └──→ AI labeling + next step (Claude)
                                              │
                                              ▼  optional, opt-in
                                        AI assistant chat (Claude)

Each stage is a separate module under src/pipeline/, and each one is independently inspectable: you can open Chrome DevTools, look at raw_events, sessions, or intent_threads directly in the Application tab, and rebuild any single stage without touching the others.

The Shared Types

Every stage consumes and produces the same handful of TypeScript interfaces, defined once in src/types.ts:

// Shared TypeScript interfaces for the openloops pipeline.
// Each stage of the pipeline consumes and produces these types.

export interface RawEvent {
  id: string;
  url: string;
  domain: string;
  title: string;
  visitedAt: number;         // epoch ms
  source: "backfill" | "live";
}

export interface Session {
  id: string;
  events: RawEvent[];
  startedAt: number;
  endedAt: number;
  domains: string[];
  keywords: string[];
}

export interface IntentThread {
  id: string;
  title: string;
  summary?: string;
  nextStep?: string;   // one concrete action to move the thread forward
  sessions: Session[];
  type: "buying" | "research" | "planning" | "learning" | "unclassified";
  confidence: number;        // 0-1
  status: "active" | "stalled" | "dormant";
  firstSeen: number;
  lastSeen: number;
  distinctDays: number;
  signals: string[];
}

export interface Brand {
  domain: string;
  name: string;
  description: string;
  industry: string;
  logoUrl: string;
  brandColor: string;
}

Most fields on IntentThread, confidence, status, signals, and distinctDays get filled in by pure local heuristics later in this guide, when you cluster and score threads. summary and nextStep stay undefined until the optional AI labeling step, covered after that, fills them in.

This is the pattern that makes the whole project work: the core data model functions on its own, and AI makes it richer.

The Manifest

openloops is a Manifest V3 extension with three permissions and three host permissions:

{
  "manifest_version": 3,
  "name": "openloops",
  "version": "0.0.1",
  "description": "Reconstruct your browsing history into an AI-labeled map of intent threads: active decisions, stalled research, open questions. Fully local.",

  "permissions": ["history", "tabs", "storage"],
  "host_permissions": [
    "https://api.anthropic.com/*",
    "https://api.context.dev/*",
    "https://logos.context.dev/*"
  ],

  "background": {
    "service_worker": "src/background.ts",
    "type": "module"
  },

  "options_page": "src/dashboard/index.html",

  "icons": {
    "16": "icons/icon16.png",
    "32": "icons/icon32.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },

  "action": {
    "default_title": "openloops",
    "default_icon": {
      "16": "icons/icon16.png",
      "32": "icons/icon32.png"
    }
  }
}

The permissions, host permissions, and options_page entry each carry specific weight:

  • permissions: ["history", "tabs", "storage"] are the only permissions the core pipeline needs. history reads your browsing history for the backfill, tabs lets the service worker observe new page loads and lets “Resume” reopen tabs, and storage is where API keys and preferences live.

  • host_permissions are separate, and only matter if you use the optional AI features. They’re what let the dashboard make fetch() calls to Anthropic and context.dev without hitting CORS errors.

  • options_page points at the dashboard. Setting it this way, instead of a default_popup, means clicking the toolbar icon opens the dashboard as a full browser tab rather than a tiny popup, which matters once you’re looking at a multi-column layout with status-grouped cards and a chat panel.

How to Scaffold the Extension

Start with Vite and the CRXJS plugin, which compiles a Manifest V3 extension with hot module reloading:

npm create vite@latest openloops -- --template react-ts
cd openloops
npm install @crxjs/vite-plugin idb react-markdown

Your vite.config.ts wires CRXJS to your manifest.json, and from there, Vite handles compiling src/background.ts to a real .js file that Chrome can load (a raw .ts service worker path in the manifest will fail with a registration error, which we’ll debug in the next section).

The dashboard’s entry point is a standard React 18 root:



  
    
    
    openloops
  
  
    
    

Scroll to Top