Objective
What you're building
Everything a real user would see and touch — 4 labeled buttons, a side panel that opens and has a chat-like layout, a settings screen that saves an API key — and none of it calls an AI model yet. If someone clicked "Summarize" right now, nothing intelligent would happen. That's correct for today.
The popup: 4 real buttons, zero wired features
function Popup() {
const openSidePanel = (action: string) => {
chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL', action });
};
return (
<div className="popup">
<h1>Page Intelligence</h1>
<button onClick={() => openSidePanel('summarize')}>Summarize this page</button>
<button onClick={() => openSidePanel('ask')}>Ask a question</button>
<button onClick={() => openSidePanel('extract')}>Extract data</button>
<button onClick={() => openSidePanel('factcheck')}>Fact-check selection</button>
</div>
);
}
Notice the popup doesn't call the AI API directly, and doesn't even do the actual work of opening the side panel itself -- it sends a message (chrome.runtime.sendMessage) to the service worker, which is the one context with the right permissions to open the side panel and coordinate everything else. This is Lesson 1's message-passing diagram, now doing real work instead of being a diagram.
Chrome's side panel API
The side panel is a persistent, first-class Chrome UI surface, not a popup workaround
Unlike a popup (which closes the instant focus leaves it), the Chrome Side Panel API gives you a UI surface that stays open alongside the page the user is browsing -- exactly what a chat-like "ask questions about this page" experience needs. chrome.sidePanel.open({ tabId }) from the service worker is the actual API call; the popup's job is just to trigger it via the message from the snippet above.
Settings: where the API key actually lives
Never hardcode an API key into extension code you ship
Anything bundled into your extension's built files can be extracted by anyone who unpacks the .crx -- a hardcoded key ships your API budget to the world the moment someone finds it. The correct pattern: a settings screen where the USER enters their own API key, saved via chrome.storage.local.set({ apiKey }), read by the service worker at call-time via chrome.storage.local.get('apiKey'). This isn't extra polish, it's the only safe way to ship an extension that calls a paid API.
A teammate suggests hardcoding your own Anthropic API key into the extension so users don't have to set anything up, since "it's just for internal testing anyway." What's the risk, and does "internal testing" change the answer?
Extension code (including a background service worker's bundled JS) is not encrypted or hidden from a determined user who unpacks the .crx or inspects the loaded extension -- "internal testing" is exactly the kind of scope that quietly expands (a teammate shares the unpacked folder, a test build gets side-loaded onto more machines than intended) which is why the settings-based pattern should be the default from the first line of code, not a "fix before shipping" TODO.
What you're building
product_shell.tsx (spanning the popup, side panel, and settings entrypoints): 4 labeled, functioning-but-unwired popup buttons, a side panel that opens via the Chrome Side Panel API with a chat-like layout, and a settings screen that saves an API key to chrome.storage.local — verified by opening the extension, clicking through all 4 buttons, and confirming the side panel opens correctly each time.