Skip to content
C.W.K.
Stream
Lesson 04 of 10 · published

Anchor 4 — Popup as a Doorway

~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.'"

The Six Lines

document.getElementById('open-panel')?.addEventListener('click', async () => {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (windowInfo?.id !== undefined) {
    await chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
  }
  window.close();
});

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 ChromeEmbed v0.2 toolbar popup: three mode buttons — Side Panel, Dock Mode, Overlay Mode — above a Frontend selector set to Auto, and a line reading that all modes use the same Pippa sidekick panel.
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
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Pippa</title>
    <style>
      body { width: 240px; padding: 12px; margin: 0; font-family: system-ui, sans-serif; }
      button { width: 100%; padding: 10px 14px; font: inherit; cursor: pointer; }
    </style>
  </head>
  <body>
    <button id="open-panel">Open Panel</button>
    <script src="popup.js"></script>
  </body>
</html>
popup.js — entire file: six lines, one job·javascript
document.getElementById('open-panel')?.addEventListener('click', async () => {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (windowInfo?.id !== undefined) {
    await chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
  }
  window.close();
});
v0.2.1 checkpoint — popup.html — three doors, one shared Sidekick·html
<button id="open-sidepanel">Side Panel</button>
<button id="open-dock">Dock Mode</button>
<button id="open-overlay">Overlay Mode</button>
<label for="panel-origin">Frontend</label>
<select id="panel-origin">
  <option value="auto">Auto</option>
</select>
<p>All modes use the same Pippa sidekick panel.</p>
v0.2.1 checkpoint — popup.js — native side panel versus content-script modes·javascript
async function setMode(mode) {
  const windowInfo = await chrome.windows.getCurrent().catch(() => null);
  if (mode === "sidepanel") {
    await chrome.storage.local.set({ pippaEmbedDisplayMode: "sidepanel" });
    await chrome.sidePanel.setOptions({
      path: "sidepanel.html?mode=sidepanel",
      enabled: true,
    });
    await chrome.sidePanel.open({ windowId: windowInfo.id });
  } else {
    await chrome.runtime.sendMessage({
      type: "pippa:set-display-mode",
      mode,
      windowId: windowInfo?.id,
    });
  }
  window.close();
}

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.