Browser extension (Manifest V3) that scans your open tabs at regular intervals, sends their titles + domains (never the full URLs) to the AI provider of your choice (Gemini, or any OpenAI-compatible API — OpenAI, OpenRouter, Groq, Ollama…), and shows:
- a focus score (0-100) with a circular gauge and color code,
- a roast: a blunt and funny — but always good-natured — comment about what you are currently doing,
- a chart of your score evolution over the day.
Works on Chrome, Edge, Brave, Opera (same build), Firefox (dedicated build) and Safari (via Apple's converter). See Installation.
Everything is 100% local: no data is sent anywhere other than the AI
endpoint you configure, and your API key never leaves your browser
(chrome.storage.local).
The popup shows the focus score (colored gauge), the current roast with its categories, and the chart of the score evolution over the day.
- TypeScript (
strictmode, noany) - Manifest V3, cross-browser (Chromium, Firefox event page, Safari)
- Vite + @crxjs/vite-plugin (MV3-aware hot-reload)
- React 18 for the popup UI
- Zod to validate the LLM's structured response
- Chart.js + react-chartjs-2 for the history chart
- chrome.storage.local for persistence (no backend)
- chrome.alarms for the periodic scan (survives service worker sleep, unlike
setInterval) - Pluggable AI providers (
src/lib/providers/): Google Gemini (gemini-2.5-flash, structured output viaresponseSchema) and any OpenAI-compatible Chat Completions endpoint (OpenAI, OpenRouter, Groq, Ollama…), called from the service worker
src/
├─ background/
│ └─ service-worker.ts # alarm, scan, AI orchestration, storage, manual scan
├─ lib/
│ ├─ tabAnalyzer.ts # tab reading + cleaning + filtering
│ ├─ providers/ # pluggable AI backends
│ │ ├─ shared.ts # prompt, Zod schema, JSON parsing, error mapping
│ │ ├─ gemini.ts # Google Gemini
│ │ ├─ openai.ts # OpenAI-compatible Chat Completions
│ │ └─ index.ts # registry, dispatcher, UI metadata, host permission
│ ├─ focusScore.ts # score smoothing (moving average) + chart buckets
│ └─ storage.ts # typed access to chrome.storage.local (+ daily reset)
├─ popup/
│ ├─ index.html
│ ├─ main.tsx
│ ├─ Popup.tsx # assembles the UI and reads storage
│ ├─ index.css
│ └─ components/
│ ├─ ScoreGauge.tsx # circular SVG gauge
│ ├─ RoastDisplay.tsx # roast + categories + time
│ ├─ HistoryChart.tsx # score chart over the day
│ └─ ApiKeySetup.tsx # provider + API key entry / editing
└─ types/
└─ index.ts # shared types
- Node.js ≥ 18
- npm
npm install
npm run buildThe dist/ folder then contains the extension ready to be loaded.
For development with hot-reload:
npm run dev(then load thedist/folder as below; @crxjs reloads automatically on every change).
Edge, Brave and Opera are Chromium-based: they load the same dist/ build as
Chrome, with no changes.
- Open the extensions page:
- Chrome:
chrome://extensions - Edge:
edge://extensions - Brave:
brave://extensions - Opera:
opera://extensions
- Chrome:
- Enable Developer mode (toggle in the top right).
- Click Load unpacked.
- Select the project's
dist/folder. - The Focus Roast icon appears in the extensions bar (pin it for easy access).
Firefox needs a slightly different manifest (an event page instead of a service
worker, plus a gecko id). A dedicated build handles that:
npm run build:firefoxThis produces a dist-firefox/ folder. Then:
- Open
about:debugging#/runtime/this-firefox. - Click Load Temporary Add-on….
- Select
dist-firefox/manifest.json. - The Focus Roast icon appears in the toolbar.
Temporary add-ons are removed when Firefox restarts. To package it for addons.mozilla.org, zip the contents of
dist-firefox/(themanifest.jsonmust be at the root of the zip):cd dist-firefox && zip -r ../focus-roast-firefox.zip .
The source code is identical for both browsers — it uses the
chrome.*namespace, which Firefox aliases. Only the manifest differs.
Safari runs Web Extensions but wraps them in a native app, so they must be converted with Apple's tool. This step requires macOS with Xcode (it cannot be done on Linux/Windows).
-
Build the Chrome package first (
npm run build). -
Convert it into an Xcode project:
xcrun safari-web-extension-converter dist --project-location safari --app-name "Focus Roast" -
Open the generated Xcode project (in
safari/), then Run it (▶). This installs the container app + extension. -
In Safari: Settings → Extensions, enable Focus Roast. During development you may also need Settings → Advanced → Show features for web developers, then Develop → Allow Unsigned Extensions.
Requires Safari 16.4+ (first version with full MV3 service-worker support). To distribute it you must sign the app with an Apple Developer account and ship it through the App Store.
On first launch, the popup shows a setup screen. Pick a provider, paste your key, and click Save.
Get a key from Google AI Studio (it
starts with AIza...).
Select OpenAI-compatible and fill in:
- API key — from your provider (e.g. OpenAI).
- Base URL — defaults to
https://api.openai.com/v1. Examples:https://openrouter.ai/api/v1,https://api.groq.com/openai/v1,http://localhost:11434/v1(Ollama). - Model — e.g.
gpt-4o-mini,llama-3.1-8b-instant,llama3.2(Ollama).
The first time you point at a non-Gemini endpoint, the browser asks for permission to reach that specific origin (requested at save time, so only the endpoint you actually use is granted).
The key is stored only in chrome.storage.local (on your machine). You can
change provider or key at any time via the ⚙ icon in the popup.
- An automatic scan runs every 5 minutes via
chrome.alarms. - Before each scan, internal tabs (
chrome://,about:blank, extension pages…) are filtered out. - Only title + domain are sent to the LLM (never the full URL with its parameters).
- The displayed score is smoothed (moving average over the latest scans) to avoid abrupt variations.
- History is kept for the current day and reset every day (at midnight, local time).
- You can force an immediate scan with the Scan now button.
| Permission | Why |
|---|---|
tabs |
Read tab titles and URLs to extract the domain. |
storage |
Persist the provider config, the last result and the history. |
alarms |
Schedule the periodic scan reliably under MV3. |
host_permissions: https://generativelanguage.googleapis.com/* |
Call the Gemini API from the service worker. |
optional_host_permissions |
Granted on demand for the OpenAI-compatible endpoint you configure (only that origin). |
- No backend, no third-party server: the only network requests go to the AI endpoint you configure (Gemini by default).
- No full URL is transmitted — only titles and domains.
- The API key and history stay local and are never exported.
| Command | Effect |
|---|---|
npm run dev |
Build in watch mode (MV3 hot-reload). |
npm run build |
Typecheck (tsc -b) + production build into dist/ (Chrome). |
npm run build:firefox |
Build, then emit a Firefox-adapted package into dist-firefox/. |
npm run typecheck |
Type checking only. |
npm run package |
Build both targets and zip them (Chrome + Firefox) at the root. |
Pre-built, ready-to-install packages for each version are attached to every
GitHub Release: focus-roast-chrome-v<version>.zip (Chrome /
Edge / Brave / Opera) and focus-roast-firefox-v<version>.zip.
Releases are automated: pushing a v<version> tag (matching package.json)
runs the release workflow, which builds both
packages, zips them and publishes a Release with auto-generated notes.
# cut a release once package.json + manifest.json are on the new version
git tag v0.2.0
git push origin v0.2.0