A panel that asks, a host that answers
My Sidekick is cwkPippa's own page, /embed/panel, and it does not know which browser it is in. It asks its host questions with window.postMessage, and whoever hosts it answers. Its first host was the family's Chrome extension. Voyager is the second, and it answers in cwkPippa's words. Voyager's house sign states the rule for every future session: a name on this wire changes only after it has changed in cwkPippa. That is what lets one panel serve two browsers without a fork.
What the panel can ask
PippaPanelRequest lists every question, and its JSON initializer returns nil for anything else, so an unknown message gets no answer at all. A request id is echoed back into the panel, so it is kept only while it is 200 characters or fewer.
| The panel asks | Voyager answers |
|---|---|
pippa:request-context | pippa:host-context with the payload for the tab in front, or pippa:no-context with a reason Dad can read |
pippa:capture-document | pippa:web-page-document: the whole page as text, with its images |
pippa:capture-screenshot | pippa:viewport-screenshot: a PNG of what Dad sees, with the view's payload beside it |
pippa:insert-text | pippa:insert-text-result, after typing into the field Dad last had focus in |
pippa:set-display-mode | pippa:display-mode-result: only sidepanel succeeds, because dock and float are not built |
pippa:close-embed | pippa:close-embed-result, and then the window closes the panel |
pippa:request-mic-permission | pippa:mic-permission-result with opened: false: there is nothing to open, because my origin already has the microphone (lesson 4) |
Insert is the one request that changes a page, which is why invariant 8 counts it as an action that happens only on Dad's word. The panel sends it when Dad presses Insert, and it names the page it expects. The script refuses if the page changed, if no field has the cursor, or if the field is a password, a code or a card number.
How a message crosses
Loaded as the top document, the panel is its own parent, so whatever it posts to its parent lands on its own window. Voyager injects a small bridge script at document start, into the main frame only. It listens for message events whose source is that same window and whose type is one of the seven requests, and forwards them through a WKScriptMessageHandler named voyagerPanel. Only requests go through. Voyager's answers travel back on the same window, as window.postMessage(json, window.location.origin), and the bridge must never hand them back to Voyager as if the panel had asked. The gates on that handler are lesson 4.
The payload, and a field named for Chrome
The answer to a context request is a WebPagePayload. Its fields carry cwkPippa's names, which is why some of them look odd in Swift:
host_kindis alwaysweb-page, andbrowser_session_idisvoyager.chrome_tab_keyandhost_idboth holdvoyager:tab:followed by the tab's UUID in lower case.source,frameUrl,human_label(the title, or the host when there is none),favicon,languageandwordCountdescribe the page.viewportText,selection, the viewport's size and scroll, and the page's full height describe the view.readableTextrepeatsviewportTextfor cwkPippa's older readers; the comment beside it warns that it holds the view, and the name promises more.account_idandaccount_labelname the identity the tab lives in, andrelated_sourcescarries the companions of lesson 4.
The tab key is the field the panel cares about most. The web-page kind has to know which tab a context belongs to, and because its first host was Chrome, the field is called chrome_tab_key. cwkPippa's panel refuses a context with no tab identity: such a context keys to the URL, and in Chrome that once made the panel flip to background tabs that were refreshing. Voyager sends no chrome_tab_id and fills chrome_tab_key with its own key. cwkPippa files each tab's Sidekick conversations under it, so switching tabs switches conversations.
Told once per page
The Sidekick does not poll. Voyager tells it when the tab in front changes, goes somewhere or finishes loading, and only once per page. frontTabChanged builds a stamp of the tab, its address, the page object, its content revision, the selection, whether sharing is on and the chosen companions. If the stamp equals the last one announced, nothing is sent. A title that changes by itself is not a new page, and cwkPippa keeps no timed refresh of its own. The read is asynchronous, so the stamp is checked again on both sides of it. If Dad moved on while the page was being read, the stale answer is dropped and the next stamp wins.
Nothing injected, ever
None of this reaches my prompt by itself. The panel stores what it is told as a snapshot in cwkPippa, and I ask for it when I want it: sidekick_host_read with the latest window for the current snapshot or the document window for the captured whole page in chunks, sidekick_host_search for matching excerpts, and sidekick_host_status for what is attached. At that boundary cwkPippa frames the page's free text, the selection and the viewport text, as untrusted evidence. A page can put any sentence it likes in front of Dad, and what reaches me is still marked as something a page said.