"ChromeEmbed's background.js is 120 lines doing one job: receive context from content scripts, hold the latest per tab, hand it to the side panel on request. Lesson 2 is the bus pattern that ties the three contexts together."
The Three Message Types
The whole bus traffics in three:
pippa:host-context— pushed by content scripts when the user scrolls, selects, or focuses. Payload is the full viewport snapshot (Lesson 6).pippa:request-context— pulled by the side panel when it wants the current context (mount, iframe load, explicit refresh).pippa:open-panel— pushed by the popup when the user clicks the action icon. The SW responds by calling chrome.sidePanel.open with a user-gesture-derived call.
That's it. No clip storage, no chat state, no soul/brain wiring — those live in cwkPippa (loaded via the panel's iframe). The SW is a thin coordinator.
The Per-Tab Map
const latestContextByTab = new Map() is the only state in the SW. Keyed by tabId, valued by the most recent host-context payload. Two writes:
- When a content script pushes
pippa:host-context, the handler merges it with the previous entry (selection persistence from earlier pushes carries forward; sub-frame pushes don't blow away top-frame state). - When the SW pulls a fresh context for the panel (
requestContextFromActiveTab), it stores the result in the Map.
One read: when the panel asks for context, the SW returns the latest cached entry while also re-querying the live content script — best of both worlds. If the SW was evicted between push and ask, the Map starts empty again; the re-query path fills it.
The Merge Logic
Worth reading carefully:
const next = incomingIsSubframe && previous
? {
...previous,
snapshot_at: incoming.snapshot_at || previous.snapshot_at,
selection: incoming.selection || previous.selection,
}
: {
...(previous || {}),
...incoming,
};
If a sub-frame is pushing (an iframe inside the user's page), don't let it blow away the top-frame's viewport text and URL. Just take its timestamp and its selection. If the top-frame pushes, accept everything as the new state. This is the kind of nuance that emerges only after you've watched real pages with iframes (Stack Overflow embeds, YouTube videos) push competing context payloads.
The Selection Fallback
readSelectionFromPage is a programmatic chrome.scripting.executeScript call. Even if the content script hasn't reported a selection (maybe it's busy, maybe it crashed), the SW can still ask Chrome to read the live selection from any frame. The fallback runs alongside the message-based content-script query, then both results merge.
Track 6's activeTab + scripting combo is what makes this work without standing host permissions. The user-gesture-triggered scripting call is exactly what activeTab covers.
The Side Panel Open Path
When the popup asks the SW to open the panel:
chrome.windows.getCurrent().then((windowInfo) => {
if (windowInfo.id !== undefined) {
chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
}
sendResponse({ ok: true });
});
The windowId form is critical — passing tabId instead would scope the panel to that one tab; windowId opens it for the whole window. The .catch(() => {}) silently absorbs the race where the user dismisses the panel before the open completes. Track 4 Lesson 2's user-gesture-rule applies: the popup click is the gesture, and the SW carries it through the message.
What's NOT in This SW
ChromeEmbed v0.1 deliberately doesn't:
- Persist contexts beyond the SW lifetime — once Chrome evicts, the Map empties.
- Maintain a history of contexts — only the latest per tab.
- Talk to cwkPippa's backend — the iframe does all real work; the SW just forwards messages.
- Implement any clip / soul / brain logic — that's the panel's iframe content, not the SW's job.
The discipline of NOT adding things keeps the SW small and obviously correct. Every line is justified by one of the three message types; nothing is there 'just in case.'