"ChromeEmbed의 background.js가 한 job 하는 120 줄: content script에서 context 받고, tab 별 최신 거 hold, request 시 side panel 한테 건넴. Lesson 2가 세 context 묶는 bus 패턴."
세 message type
전체 bus가 세 가지 traffic:
pippa:host-context— user가 scroll, select, focus 할 때 content script가 push. Payload가 full viewport snapshot (Lesson 6).pippa:request-context— 현재 context 원할 때 (mount, iframe load, 명시적 refresh) side panel이 pull.pippa:open-panel— user가 action icon click 할 때 popup이 push. SW가 user-gesture-derive 된 호출로 chrome.sidePanel.open 호출해서 응답.
그게 전부. Clip storage 없음, chat 상태 없음, soul/brain wiring 없음 — 그것들이 cwkPippa (panel iframe 통해 load) 에 살아. SW가 얇은 coordinator.
Per-tab Map
const latestContextByTab = new Map()가 SW의 유일한 상태. tabId로 key, 가장 최근 host-context payload로 value. 두 write:
- Content script가
pippa:host-contextpush 할 때 handler가 이전 entry와 merge (이전 push에서 selection persistence carry forward. sub-frame push가 top-frame 상태 blow away 안 함). - SW가 panel 위해 fresh context pull (
requestContextFromActiveTab) 할 때 결과를 Map 에 저장.
한 read: panel이 context 요청할 때 SW가 가장 최근 cached entry 반환하면서 live content script 도 re-query — best of both worlds. SW가 push와 ask 사이 evict 됐으면 Map이 빈 채로 다시 시작. re-query 경로가 fill.
Merge logic
주의 깊게 read 가치:
const next = incomingIsSubframe && previous
? {
...previous,
snapshot_at: incoming.snapshot_at || previous.snapshot_at,
selection: incoming.selection || previous.selection,
}
: {
...(previous || {}),
...incoming,
};
Sub-frame이 push 하면 (user page 안 iframe), top-frame의 viewport 텍스트와 URL를 blow away 안 시킴. 그것의 timestamp와 selection 만 take. Top-frame이 push 하면 모든 것 새 상태로 받아들임. 이게 iframe 가진 실제 page (Stack Overflow embed, YouTube video) 가 경쟁 context payload push 하는 거 본 후에만 등장하는 nuance.
Selection fallback
readSelectionFromPage는 chrome.scripting.executeScript를 직접 부르는 길이야. content script가 선택을 안 올려 줬더라도 — 바쁘거나 죽었을 수도 있으니까 — SW가 Chrome 한테 어느 frame 이든 지금 선택을 읽어 달라고 부탁할 수 있어. 이 보조 경로가 메시지로 물어보는 길과 나란히 돌면서,다음 두 결과 merge.
Track 6의 activeTab + scripting combo가 이걸 standing host permission 없이 동작하게. User-gesture-trigger 된 scripting 호출이 정확히 activeTab cover.
Side panel open 경로
Popup이 SW 한테 panel 열기 요청할 때:
chrome.windows.getCurrent().then((windowInfo) => {
if (windowInfo.id !== undefined) {
chrome.sidePanel.open({ windowId: windowInfo.id }).catch(() => {});
}
sendResponse({ ok: true });
});
windowId form이 critical — tabId 대신 전달하면 그 한 tab 으로 panel scope. windowId가 전체 window 위해 열기. .catch(() => {})가 open 완료 전 user가 panel dismiss 하는 race를 silently 흡수. Track 4 Lesson 2의 user-gesture-rule 적용: popup click이 gesture, SW가 message 통해 carry.
이 SW 에 없는 것
ChromeEmbed v0.1이 일부러 안 하는 것:
- SW lifetime 너머 context persist — Chrome evict 하면 Map 비움.
- Context history 유지 — tab 당 최신만.
- cwkPippa의 backend와 대화 — iframe이 모든 real work, SW는 message 만 forward.
- clip 이나 soul, brain 로직을 여기 넣는 것 — 그건 panel iframe 안쪽이 할 일이지 SW 몫이 아니야.
안 넣는 절제가 SW를 작게, 그리고 한눈에 맞다고 알아볼 수 있게 지켜 줘. 여기 있는 모든 줄이 메시지 세 종류 중 하나로 설명이 되고, '혹시 몰라서' 남아 있는 건 하나도 없어.