experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(broadcaster): echo-prevention by full URL, not peek host

The setExtensionBroadcaster echo-prevention compared the source
URL peek host (the tile id) against each window peek host. That
correctly excluded the sender from receiving its own message but
also excluded every OTHER window of the same feature tile.

A feature with both a background and a window entry (websearch is
the canonical case: peek://websearch/background.html plus
peek://websearch/home.html) has two BrowserWindows sharing the
peek host websearch. The host-level compare treated them as the
same sender and silently dropped every cross-window round-trip
between them. Home publishes websearch:engine-request, bg never
receives. Bg publishes websearch:engines-list, home never receives.
Same class of bug as the earlier webview-guest reach fix.

Fix: compare the full URL (peek://websearch/home.html) against
the full source address (peek://websearch/home). The only URL
that matches the source is the exact originating window, so
sibling entries of the same feature receive their events while
echoes are still suppressed. Applied to both the BrowserWindow
pass and the webview-guest pass. Removed the no-longer-used
peekHost helper.

Also flags a follow-up in docs/tasks.md: websearch should merge
its background tile into the home window entirely, using the
single-file tiles (resident true) model. Cross-window pubsub
within one feature is fragile. This fix unblocks the broadcaster
but the root design issue is still there.

Tests: HUD Extension 10/10 still passing (the fix is stricter
but equivalent for their case since HUD widgets have distinct
URLs already). Websearch cluster (3 tests) still failing.
Different root cause: the websearch bg tile is receiving
tile:shutdown mid-test and tearing down its subscriptions
before home can round-trip. Separate bug, out of scope here.

+13 -20
+12 -20
backend/electron/main.ts
··· 196 196 // Hybrid mode: broadcast to BOTH consolidated (iframes) AND separate windows 197 197 // All send() calls are wrapped in try-catch to prevent errors during shutdown 198 198 // when windows are being destroyed concurrently (race between isDestroyed check and send). 199 - // Extract the host (tile id) from a peek://host/path source string. 200 - // Node's URL.origin returns "null" for non-standard schemes regardless of 201 - // Electron's standard:true registration, which would make every peek:// 202 - // window appear to share an origin with every other — silently dropping 203 - // every cross-tile pubsub message. Parse the host manually instead. 204 - function peekHost(s: string): string { 205 - if (!s.startsWith('peek://')) return s; 206 - const rest = s.slice('peek://'.length); 207 - const slash = rest.indexOf('/'); 208 - return slash === -1 ? rest : rest.slice(0, slash); 209 - } 210 199 setExtensionBroadcaster((topic, msg, source) => { 211 - const sourceOrigin = peekHost(source); 212 - 213 200 // Broadcast to v2 tile BrowserWindows (launched by tile-launcher). 214 201 // Without this, v2 tiles that subscribe to global pubsub events (editor:open, 215 202 // item:created, etc.) never receive them. 203 + // 204 + // Echo-prevention compares FULL source URL against FULL window URL rather 205 + // than just the peek host. One feature tile can have multiple 206 + // BrowserWindows sharing the same host (e.g. websearch has peek://websearch/ 207 + // background.html AND peek://websearch/home.html). A host-level compare 208 + // treats those siblings as the same sender and silently drops their 209 + // intra-feature pubsub round-trips. Full-URL compare only excludes the 210 + // exact originating window, allowing sibling entries to receive. 216 211 for (const tileWin of getAllTileWindows()) { 217 212 try { 218 - const winHost = peekHost(tileWin.webContents.getURL()); 219 - // Don't echo back to sender (matched by tile host, not URL.origin — 220 - // see peekHost comment above). 221 - if (winHost !== sourceOrigin) { 213 + const winUrl = tileWin.webContents.getURL(); 214 + if (winUrl !== source) { 222 215 tileWin.webContents.send(`pubsub:${topic}`, { 223 216 ...(msg as object), 224 217 source ··· 236 229 // only tracks top-level BrowserWindows), so without this second pass 237 230 // their subscribers never fire. Filter by `getType() === 'webview'` 238 231 // and peek:// URLs to avoid broadcasting into arbitrary http(s) 239 - // content webviews. 232 + // content webviews. Same full-URL echo-prevention as above. 240 233 for (const wc of webContents.getAllWebContents()) { 241 234 try { 242 235 if (wc.isDestroyed()) continue; 243 236 if (wc.getType() !== 'webview') continue; 244 237 const url = wc.getURL(); 245 238 if (!url.startsWith('peek://')) continue; 246 - const wcHost = peekHost(url); 247 - if (wcHost !== sourceOrigin) { 239 + if (url !== source) { 248 240 wc.send(`pubsub:${topic}`, { 249 241 ...(msg as object), 250 242 source
+1
docs/tasks.md
··· 10 10 11 11 ## Tile architecture cleanup (post-conversion) 12 12 13 + - [ ] **Merge websearch's separate background tile into the home window.** Manifest has two tile entries: `background.html` (lazy:true) for settings/engine state, `home.html` for the UI. They communicate via pubsub round-trips (`websearch:engine-request` → `websearch:engines-list`) across `peek://ext/websearch/background.html` vs `peek://websearch/home.html`. Cross-window pubsub within one feature is fragile (see 2026-04-20 session — broadcaster echo-prevention bug + cluster 3 regression risk). The round-trip also blocks 3 websearch tests from passing. With the single-file tiles model shipping (`resident: true`), websearch can collapse to one tile whose home window owns both UI and engine state directly. No IPC needed. Applies also to any feature tile with a bg + window pair that pubsubs between itself — audit other candidates. 13 14 - [ ] **Remove v1 entirely.** Single source of truth: every renderer runs through `tile-preload.cts` against `tile:*` IPC. No more consolidated extension host iframes, no more legacy IPC channels, no more dual paths in tile-preload. See [v1-removal-plan.md](v1-removal-plan.md). This is the root fix for the v2→v1 routing flakies (kagi/tag/widget-update tests, ~10 tests blocked) — the boundary disappears, the bugs disappear with it. Phased: (1) cmd/hud/page core scripts → tile-preload, (2) test fixture → tile-preload, (3) delete v1 plumbing, (4) manifest cleanup. ~5–6 days. 14 15 - [x] **Single-file tiles as default model.** Shipped 2026-04-17: `resident: true` marker + flat window fields on TileEntry, `api.window.showSelf/hideSelf` (token-scoped, no capability), hello-world/widget-demo/mcp-server converted. Plan: `docs/tiles-single-file.md`. 15 16 - [ ] **Theme live-reload in open tiles.** When the user switches theme, tiles that are already open don't pick up the new `peek://theme/variables.css`. The import is resolved once at tile load. `api.theme.onChange` hook exists but nothing re-imports the stylesheet. Fix: on theme change, have tiles re-resolve `peek://theme/variables.css` (e.g. bump a cache-busting query string on a `<link>` element, or replace the adopted stylesheet). Alternatively, inject theme vars via a mutable `<style>` block that the preload updates on theme change.