~8 min · popup, side-panel, case-study, single-responsibility, v0.2.1
Level 0Extension Curious
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"ChromeEmbed v0.1's popup.js was six lines. Lesson 4 is why that was the right number — and what happens when you reach for 'just one more popup feature.'"
One handler. Click 'Open Panel' button → fetch current window → open side panel in that window → close the popup. The popup HTML is comparably tiny — a styled button labeled 'Open Panel.'
Why This Is Right
Single responsibility — the popup is a launcher. Anything more would compete with the panel as the actual experience surface.
Predictable lifecycle — popup opens, popup closes. No state to manage, no rerender to handle, no race conditions to debug.
Fast paint — the popup HTML loads, JS runs, click handler attaches. Probably under 50 ms from open to ready.
No competing UI — Pippa lives in the panel iframe; the popup pointing the user toward it (rather than hosting parallel chat UI) keeps the mental model singular.
The Alternative That Doesn't Work
What if you wanted the popup to be useful? You'd add quick-chat input, a most-recent-message preview, a brain selector. Each of those:
Duplicates UI that already exists in the panel.
Needs to message the cwkPippa backend, which the panel iframe already does — so you'd either re-implement that messaging in the popup or route through the SW → iframe (which doesn't have a clean API today).
Has lifecycle problems: the popup dies the moment the user does anything outside it, so any operation in flight when they click elsewhere is lost.
The 'useful popup' design always ends up paying duplication costs that the doorway-popup avoids entirely. ChromeEmbed's choice: don't pay the cost.
The Window vs Tab Choice
The popup passes { windowId } to chrome.sidePanel.open. Two consequences:
The panel opens for the entire window. Tab switching inside the window keeps the panel open with content updating per the active tab (via the background bus + Lesson 5's bridge).
The user can still close the panel via Chrome's standard side-panel chrome.
Passing { tabId } instead would scope the panel to that one tab; the moment the user switches tabs, the panel closes. That's the wrong UX for a household extension that wants to be ambient across the entire window's browsing session.
The Optional Chaining
document.getElementById('open-panel')?.addEventListener(...) — the optional chaining is paranoia. If the popup HTML is changed and the button id mis-spelled, the popup degrades gracefully (no JS error, just a non-functional button). For a 6-line script that's overkill; for an extension you might update from a hundred git revisions later, it's a courtesy. Keep it.
Popup as doorway, panel as destination. Six lines is enough; more would compete with the panel for the same workflow space. The cost of NOT adding popup features is exactly zero; the cost of adding them is duplication forever.
If you ever needed popup status info... The right move is to render a tiny preview in the popup (one line: 'Currently capturing context from this tab') without inviting interaction. Read from chrome.storage or message the SW; render once on open; let the user click 'Open Panel' to actually do anything. Don't add input fields, don't add chat — keep the popup as a status-and-launcher only.
The v0.2 popup — still a doorway, but now with three doors and a note that they all lead to the same room.
Code
popup.html — entire file: one button, scoped styles·html
Open the live popup.html and popup.js. Trace the Side Panel button and the Dock button separately. Identify where the direct user gesture is consumed, where the display preference is persisted, where the content script is contacted, and why popup.html contains only the Auto origin literal.
Hint
If a proposed popup feature needs conversation state or renders message history, it belongs in /embed/panel. Mode and endpoint selection are doorway responsibilities.
Progress
Progress is local-only — sign in to sync across devices.