More than one page, on purpose
Sometimes Dad wants me to weigh the page in front against a couple of others: a product page and two reviews, an article and its sources. Voyager never scans every tab to make that possible. The Sidekick's header has a chooser, "Choose up to 3 other tabs in this account", and only what Dad picks there travels with the page in front. Each pick is a companion source.
relatedSourceOptions decides what the chooser may even offer. A candidate must:
- be another tab in the same window, in the same identity as the tab in front;
- be a normal tab, never Incognito, with sharing switched on;
- hold an awake page that has finished loading, so a sleeping tab is never woken to be read;
- show an http or https address with no user name or password inside it;
- not be my own panel's page.
Choices belong to the tab in front. Switch to another tab and its own choices apply, usually none. They last only for the panel's session, so closing the panel forgets them. "Use this tab only" clears them, and because every context carries the list, the next one says related_sources: [] in so many words. The "Share this tab with Pippa" switch is a per-tab pause that survives moving the tab to another window and restoring the session.
Every await is a chance for the world to move
Reading four pages takes several trips into WebKit, and Dad can click, scroll or navigate during any of them. contextReply treats that as the normal case. It takes a stamp first: the tab in front, its address, its page object and content revision, the selection, the sharing switch, and the same facts for every chosen companion. It reads the front page and checks the stamp. For each companion it checks the stamp, reads, checks the stamp again, and checks that the address it read is the one the tab shows. Any mismatch ends the whole answer with "A selected source changed or could not be read. Refresh or deselect it." There is no partial answer with three fresh pages and one stale one. The test relatedSourceNavigationDuringReadDiscardsTheWholeSnapshot navigates a companion in the middle of its read and expects nothing back.
cwkPippa does not take Voyager's word for any of it. Its own normalizer keeps at most three related sources, and only ones in the same account, the same browser session and a normal window, each with an http or https address, no credentials, a tab key of its own and no nested set inside. Each source's text is then framed as untrusted evidence, as the page in front is. My panel shows the account and each chosen source by its label, and a search over the snapshot keeps saying which source a line came from, so I can tell Dad which page said what. Both ends of the wire count to three.
The gates on the panel's bridge
Lesson 2 showed the bridge that hands my panel's requests to Voyager. Every message that arrives there is measured by PanelMessageGate, a plain value with one field per condition:
- it came from the panel's own web view;
- it came from the main frame, never from a frame inside my page;
- the frame that spoke is on my origin;
- the web view is on my origin right now;
- it is no larger than 256 KB.
Because the gate is a value, each condition has its own test. eachGateRefusesOnItsOwn starts from a message that passes and breaks one field at a time, including a frame on my host with the wrong port or the wrong scheme. The panel also keeps itself on one origin as it navigates. A frame inside my page may load what my page gives it, since it cannot reach the bridge. The panel itself stays on my origin, and anything else it is sent to opens as a tab. Its site data is a store of its own with a fixed identifier, apart from every identity of Dad's.
A microphone for my voice only
My voice mode needs the microphone, and a permission prompt every time Dad talks to me would be exactly the kind of friction Voyager was built to remove. So a panel's page hears Dad without a prompt, under one rule: captureDecision grants capture only to the main frame on my own origin and refuses everything else in the panel. A frame inside my page cannot borrow the grant, and neither can any other origin. The test onlyPippasOwnPageHearsDad holds all three cases. It is also why pippa:request-mic-permission is answered with opened: false: there is no separate page to open, because the grant already sits where it belongs.