Browser extensions are the perfect vibe coding project. They are small in scope, have a clear structure, give you instant visual feedback, and the manifest format is strict enough that AI generates correct configurations most of the time.
In this article, you will build a Chrome extension with AI — from the first prompt to publishing on the Chrome Web Store. The tool: Cursor as the primary AI editor.
The project: PR Summary — a GitHub pull request summarizer that adds a one-click summary button to PR pages. When you click it, it extracts the PR diff, sends it to an AI API, and displays a plain-English summary right on the page.
Step 1: Describe the Extension
Prompt to Cursor:
I want to build a Chrome extension called "PR Summary"
that adds a summary button to GitHub pull request pages.
When clicked, it:
1. Extracts the PR diff from the page
2. Sends it to an API endpoint for summarization
3. Shows the summary in a popup panel on the page
Extension structure:
- Manifest V3 (required for Chrome in 2026)
- Content script: runs on github.com PR pages
- Service worker: handles API communication
- Popup: settings page (API key configuration)
- Storage: Chrome storage API for settings
TypeScript with esbuild for bundling.
Create the project structure and manifest.json first.
Do not implement the functionality yet.
Cursor created a clean project structure:
pr-summary/
├── src/
│ ├── content/
│ │ ├── content.ts # Content script (runs on PR pages)
│ │ └── styles.css # Injected styles
│ ├── background/
│ │ └── service-worker.ts # Background service worker
│ ├── popup/
│ │ ├── popup.html # Settings popup
│ │ ├── popup.ts # Popup logic
│ │ └── popup.css # Popup styles
│ └── shared/
│ ├── types.ts # Shared TypeScript types
│ └── storage.ts # Chrome storage wrapper
├── manifest.json
├── esbuild.config.ts
├── tsconfig.json
└── package.json
What I reviewed:
The manifest.json is the most important file. One wrong permission and your extension either breaks or gets rejected from the Chrome Web Store.
{
"manifest_version": 3,
"name": "PR Summary",
"version": "1.0.0",
"description": "One-click summaries for GitHub pull requests",
"permissions": ["storage", "activeTab"],
"host_permissions": ["https://github.com/*"],
"background": {
"service_worker": "dist/service-worker.js"
},
"content_scripts": [{
"matches": ["https://github.com/*/pull/*"],
"js": ["dist/content.js"],
"css": ["dist/styles.css"]
}],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
}
What I changed in the manifest:
- Removed the
"tabs"permission. Cursor added it but we only need"activeTab"— less permissions means easier Chrome Web Store review. - Changed
host_permissionsfrom"*://*/*"(all sites) to"https://github.com/*"(only GitHub). Never request more permissions than you need. - Added specific URL patterns to
content_scripts.matchesso the script only loads on PR pages, not all GitHub pages.
Step 2: Manifest V3 Challenges
Manifest V3 introduced significant changes from V2. These are the areas where AI struggles the most, because much of the training data still reflects V2 patterns.
Service workers terminate when idle. This is the biggest change from V2. Background pages in V2 were persistent — they stayed alive as long as the extension was running. Service workers in V3 terminate after 30 seconds of inactivity.
AI often generates code that stores state in global variables inside the service worker. This works in testing but fails in production because the variables are wiped when the worker restarts.
What AI got wrong:
// AI generated this — BROKEN in Manifest V3
let pendingSummaries: Map<string, string> = new Map();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'SUMMARIZE') {
pendingSummaries.set(sender.tab!.id!.toString(), message.diff);
// ... process ...
}
});
The pendingSummaries map is lost when the service worker terminates. I changed it to use chrome.storage.session (a V3 API for temporary storage that persists across service worker restarts):
// Fixed — uses chrome.storage.session
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'SUMMARIZE') {
chrome.storage.session.set({
[`pending_${sender.tab!.id}`]: message.diff
});
// ... process ...
}
return true; // Keep the message channel open for async response
});
The return true pattern. Another V3 gotcha — if your message listener does async work (like calling an API), you must return true from the listener to keep the message channel open. AI sometimes forgets this, causing the content script to receive undefined as the response.
declarativeNetRequest instead of webRequest. V3 replaced the blocking webRequest API with declarativeNetRequest. Our extension does not need request modification, but if yours does, be aware that AI will often generate V2 webRequest code that simply does not work in V3.
Step 3: Content Script — Inject the Summary Button
Prompt:
Implement the content script.
When the page is a GitHub PR (URL matches /pull/\d+):
1. Find the PR header area
2. Add a "Summarize" button next to the existing buttons
3. Style it to match GitHub's button style
4. On click: extract the PR title, description, and
file changes from the page
5. Send the data to the service worker via
chrome.runtime.sendMessage
6. Show a loading spinner while waiting
7. Display the summary in a floating panel below the
button when the response arrives
For extracting file changes, use the DOM to get the
diff content from the "Files changed" tab. If the diff
is too large (over 10,000 characters), truncate it
and add a note.
What AI handled well:
- DOM selection for GitHub’s PR page structure
- Message passing between content script and service worker
- CSS styling that matched GitHub’s design system
- The loading spinner animation
What I fixed:
GitHub’s dynamic page loading. GitHub uses Turbo (previously pjax) for navigation. The content script runs on initial page load, but when you navigate between PRs, the page updates without a full reload. AI did not account for this. I added a
MutationObserverto detect page changes and re-inject the button.Shadow DOM for styling isolation. The summary panel’s CSS conflicted with GitHub’s styles. I wrapped the panel in a Shadow DOM to isolate the styles. AI generated the panel as regular DOM elements without isolation.
Diff extraction from the “Files changed” tab. The tab content loads lazily. If the user has not clicked “Files changed” yet, the diff is not in the DOM. I added a fallback that uses the GitHub API to fetch the diff instead of scraping it from the DOM.
Step 4: Service Worker — API Communication
Prompt:
Implement the service worker.
On receiving a SUMMARIZE message:
1. Get the API key from chrome.storage.sync
2. If no API key, send back an error asking the user
to configure it in the popup
3. Send the PR data to the OpenAI API
(POST https://api.openai.com/v1/chat/completions)
4. Use gpt-4o-mini for cost efficiency
5. System prompt: "Summarize this pull request in 3-5
bullet points. Focus on what changed and why.
Be concise."
6. Send the summary back to the content script
7. Handle errors: network failure, invalid API key,
rate limiting
Use fetch — no external libraries in service workers.
Claude Code generated clean API communication code. The error handling was comprehensive.
What I fixed:
- Added a timeout to the fetch call. The default fetch has no timeout, and a slow API response could leave the user staring at a spinner forever. I used
AbortControllerwith a 30-second timeout. - Added response streaming. Instead of waiting for the full response, I switched to the streaming API so the summary appears word by word. This feels much faster to the user.
Step 5: Test in Chrome
Testing Chrome extensions requires loading them manually.
Prompt:
Create a build script and testing instructions.
Build:
- esbuild bundles TypeScript files to dist/
- Copy manifest.json, popup HTML, CSS, and icons to dist/
- Source maps for development
- Minified build for production
Testing steps:
- How to load the extension in Chrome
- How to debug the content script
- How to debug the service worker
- How to test the popup
Cursor generated a clean esbuild configuration and a README section with testing steps.
Testing checklist I used:
- Load unpacked extension from
dist/folder - Open a GitHub PR page — verify the button appears
- Click “Summarize” without an API key — verify the error message
- Add API key in popup — verify it saves
- Click “Summarize” with API key — verify the summary appears
- Navigate to another PR (without full page reload) — verify the button appears again
- Close and reopen Chrome — verify the API key persists
- Open DevTools > Application > Service Workers — verify the worker registers
Issues found during testing:
- The button did not appear on PRs with very long titles because the header layout shifted. I used a more robust CSS selector.
- The service worker showed a “service worker was terminated” warning after 30 seconds of idle time. This is normal in V3 — not a bug.
Step 6: Publish to Chrome Web Store
Prompt:
Prepare the extension for Chrome Web Store submission.
Create:
- Privacy policy (the extension sends PR diffs to
OpenAI API — disclose this)
- Store listing description (under 132 characters for
the short description)
- Screenshot specifications
- Build command that creates a zip file for upload
List the Chrome Web Store review requirements that
commonly cause rejections.
Privacy considerations for browser extensions:
Browser extensions have access to page content. Our extension reads GitHub PR pages — that is user data. The privacy policy must disclose:
- What data is collected (PR diffs and titles)
- Where it is sent (OpenAI API)
- What is stored locally (API key in chrome.storage.sync)
- What is not collected (no browsing history, no personal data beyond PR content)
Common Chrome Web Store rejection reasons AI helped me avoid:
- Excessive permissions. We already fixed this by using
activeTabinstead oftabs. - Missing privacy policy. Required if the extension sends any data to external services.
- Misleading description. The store description must accurately describe what the extension does.
- Broken functionality. The extension must work as described. Test thoroughly.
Why Browser Extensions Are Perfect for Vibe Coding
After building CLI tools, APIs, web apps, and mobile apps with AI, browser extensions hit the sweet spot:
- Small scope. A useful extension is 5-15 files. AI handles small projects better than large ones.
- Clear structure. Manifest V3 defines the architecture — content scripts, service workers, popup. AI knows exactly what goes where.
- Fast feedback. Reload the extension in Chrome and see changes instantly. No build pipeline, no deployment.
- Real users fast. The Chrome Web Store lets you publish and get users within days.
The main challenge is Manifest V3’s service worker model. Once you understand that service workers are ephemeral, the rest is straightforward.
Time Tracking
| Step | Time | AI vs Manual |
|---|---|---|
| Project structure + manifest | 8 min | AI: 5 min, Review: 3 min |
| Content script + UI | 25 min | AI: 12 min, Fix: 13 min |
| Service worker + API | 15 min | AI: 8 min, Fix: 7 min |
| Build configuration | 5 min | AI: 3 min, Fix: 2 min |
| Testing + bug fixes | 15 min | Manual |
| Store preparation | 10 min | AI: 5 min, Review: 5 min |
| Total | 78 min | AI: 33 min, Human: 45 min |
Key Takeaways
- Manifest V3 is required in 2026. AI sometimes generates V2 code. Always verify the manifest version and check for persistent background page patterns.
- Service workers are ephemeral. Never store state in global variables. Use
chrome.storage.sessionorchrome.storage.local. - Request minimal permissions. Use
activeTabinstead oftabs. Use specific host patterns instead of*://*/*. This speeds up Chrome Web Store review. - Handle dynamic page loading. Modern web apps use client-side navigation. Your content script needs a
MutationObserveror similar mechanism to detect page changes. - Disclose data usage. If your extension sends data to external services, you need a privacy policy. Be transparent about what data is collected and where it goes.
What’s Next?
In the next article, you will explore local AI models for coding. You will set up Ollama, run code-focused models on your own machine, and connect them to Cursor — all without sending your code to the cloud.
Part 19 of the Vibe Coding series.