experiments in a post-browser web
10
fork

Configure Feed

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

feat(pubsub): Phase 1 — fix bgWindow broadcast + rename broadcaster

+96 -18
+1 -1
backend/electron/index.ts
··· 131 131 subscribe, 132 132 unsubscribe, 133 133 unsubscribeAll, 134 - setExtensionBroadcaster, 134 + setPubsubBroadcaster, 135 135 getSystemAddress, 136 136 } from './pubsub.js'; 137 137
+1 -1
backend/electron/ipc.ts
··· 1411 1411 1412 1412 // Register canvas page host windows with the tile broadcaster so pubsub 1413 1413 // messages (tag:item-added, sync:pull-completed, etc.) are forwarded to 1414 - // page.js via the same extensionBroadcaster path used by feature tiles. 1414 + // page.js via the same pubsubBroadcaster path used by feature tiles. 1415 1415 // Token cleanup (revokeToken) happens automatically on 'closed'. 1416 1416 if (useCanvas && pageHostToken && pageHostEntryId) { 1417 1417 registerTrustedBuiltinWindow(PAGE_HOST_TILE_ID, pageHostEntryId, win, pageHostToken);
+33 -4
backend/electron/main.ts
··· 21 21 import { installLoadOnDispatchHook } from './tile-lazy.js'; 22 22 import { initTray } from './tray.js'; 23 23 import { registerLocalShortcut, unregisterLocalShortcut, handleLocalShortcut, registerGlobalShortcut, unregisterGlobalShortcut, unregisterShortcutsForAddress } from './shortcuts.js'; 24 - import { scopes, publish, subscribe, unsubscribe, hasSubscriber, setExtensionBroadcaster, getSystemAddress } from './pubsub.js'; 24 + import { scopes, publish, subscribe, unsubscribe, hasSubscriber, setPubsubBroadcaster, getSystemAddress } from './pubsub.js'; 25 25 import { WEB_CORE_ADDRESS, isTestProfile, isDevProfile, isEphemeralProfile, isHeadless, getProfile, setTilePreloadPath, getTilePreloadPath, DEBUG } from './config.js'; 26 26 import { getSystemThemeBackgroundColor } from './windows.js'; 27 27 import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js'; ··· 209 209 const dbPath = path.join(config.userDataPath, config.profile, 'datastore.sqlite'); 210 210 initDatabase(dbPath); 211 211 212 - // Set up extension broadcaster for pubsub 213 - // Hybrid mode: broadcast to BOTH consolidated (iframes) AND separate windows 212 + // Set up pubsub broadcaster. 213 + // Forwards GLOBAL publishes to every renderer that needs to see them: 214 + // bgWindow (core), tile BrowserWindows, and peek:// webview guests. 214 215 // All send() calls are wrapped in try-catch to prevent errors during shutdown 215 216 // when windows are being destroyed concurrently (race between isDestroyed check and send). 216 - setExtensionBroadcaster((topic, msg, source) => { 217 + setPubsubBroadcaster((topic, msg, source) => { 218 + // Broadcast to bgWindow (core orchestrator). 219 + // bgWindow hosts app/index.js which runs cmd/page/hud registries and is a 220 + // first-class pubsub subscriber. Every publish must reach it — historical 221 + // omissions from this loop were directly responsible for "hello-world-only 222 + // commands visible in cmd panel" and "v2 tile cmd:execute:*:result never 223 + // reaches core subscribers" bug classes. bgWindow is NOT a tile; it is 224 + // broadcast to explicitly rather than via getAllTileWindows(). 225 + const bgWindow = getCoreBackgroundWindow(); 226 + if (bgWindow && !bgWindow.isDestroyed()) { 227 + try { 228 + const winUrl = bgWindow.webContents.getURL(); 229 + if (winUrl !== source) { 230 + bgWindow.webContents.send(`pubsub:${topic}`, { 231 + ...(msg as object), 232 + source 233 + }); 234 + } 235 + } catch { 236 + // Window may have been destroyed between check and send 237 + } 238 + } 239 + 217 240 // Broadcast to v2 tile BrowserWindows (launched by tile-launcher). 218 241 // Without this, v2 tiles that subscribe to global pubsub events (editor:open, 219 242 // item:created, etc.) never receive them. 220 243 // 244 + // Skip bgWindow if it appears in the tile registry — it is registered there 245 + // via `registerTrustedBuiltinWindow` for lifecycle/cleanup bookkeeping, but 246 + // we already sent to it above. Without this guard, bgWindow would receive 247 + // each frame twice. 248 + // 221 249 // Echo-prevention compares FULL source URL against FULL window URL rather 222 250 // than just the peek host. One feature tile can have multiple 223 251 // BrowserWindows sharing the same host (e.g. websearch has peek://websearch/ ··· 226 254 // intra-feature pubsub round-trips. Full-URL compare only excludes the 227 255 // exact originating window, allowing sibling entries to receive. 228 256 for (const tileWin of getAllTileWindows()) { 257 + if (tileWin === bgWindow) continue; 229 258 try { 230 259 const winUrl = tileWin.webContents.getURL(); 231 260 if (winUrl !== source) {
+48
backend/electron/pubsub.test.ts
··· 146 146 pubsub.unsubscribe('peek://real/', 'test:has:excl'); 147 147 }); 148 148 }); 149 + 150 + describe('setPubsubBroadcaster', () => { 151 + it('GLOBAL publishes invoke the registered broadcaster with topic/msg/source', () => { 152 + const calls: Array<{ topic: string; msg: unknown; source: string }> = []; 153 + pubsub.setPubsubBroadcaster((topic, msg, source) => { 154 + calls.push({ topic, msg, source }); 155 + }); 156 + pubsub.publish('peek://tile-foo/entry', pubsub.scopes.GLOBAL, 'test:broadcaster:fanout', { n: 1 }); 157 + assert.strictEqual(calls.length, 1); 158 + assert.strictEqual(calls[0].topic, 'test:broadcaster:fanout'); 159 + assert.deepStrictEqual(calls[0].msg, { n: 1 }); 160 + assert.strictEqual(calls[0].source, 'peek://tile-foo/entry'); 161 + // Tear down so other tests aren't affected. 162 + pubsub.setPubsubBroadcaster(() => {}); 163 + }); 164 + 165 + it('SELF-scoped publishes do NOT invoke the broadcaster (broadcaster is for GLOBAL fan-out only)', () => { 166 + let called = false; 167 + pubsub.setPubsubBroadcaster(() => { called = true; }); 168 + pubsub.publish('peek://tile-foo/entry', pubsub.scopes.SELF, 'test:broadcaster:self', {}); 169 + assert.strictEqual(called, false); 170 + pubsub.setPubsubBroadcaster(() => {}); 171 + }); 172 + 173 + it('broadcaster receives a v2-tile publish of cmd:execute:*:result so bgWindow-subscriber dispatchers can resolve', () => { 174 + // This is the Phase-1 regression guard. A v2 tile publishes a command 175 + // result topic via GLOBAL scope. The main.ts broadcaster callback is 176 + // responsible for forwarding this to bgWindow (core), which is where 177 + // the dispatcher that set `resultTopic` lives. If the broadcaster is 178 + // not invoked for GLOBAL publishes, bgWindow never resolves the 179 + // pending result promise and tag/untag/widget-update smoke tests 180 + // time out at 10s. 181 + const received: Array<{ topic: string; source: string }> = []; 182 + pubsub.setPubsubBroadcaster((topic, _msg, source) => { 183 + received.push({ topic, source }); 184 + }); 185 + pubsub.publish( 186 + 'peek://tags/background', 187 + pubsub.scopes.GLOBAL, 188 + 'cmd:execute:tag:result:abc-123', 189 + { ok: true }, 190 + ); 191 + assert.strictEqual(received.length, 1); 192 + assert.strictEqual(received[0].topic, 'cmd:execute:tag:result:abc-123'); 193 + assert.strictEqual(received[0].source, 'peek://tags/background'); 194 + pubsub.setPubsubBroadcaster(() => {}); 195 + }); 196 + }); 149 197 });
+9 -9
backend/electron/pubsub.ts
··· 4 4 * Handles: 5 5 * - Topic-based publish/subscribe 6 6 * - Scope-based message filtering (SYSTEM, SELF, GLOBAL) 7 - * - Extension window broadcasting (via callback) 7 + * - Renderer window broadcasting (via callback) 8 8 * 9 9 * NOTE: The scope/topic logic is mirrored in app/lib/pubsub.js (the shared 10 10 * renderer-layer PubSub engine). This file stays separate because the main ··· 27 27 // Topic subscribers: topic -> Map<source, callback> 28 28 const topics = new Map<string, Map<string, (msg: unknown) => void>>(); 29 29 30 - // Callback for broadcasting to extension windows 31 - let extensionBroadcaster: ((topic: string, msg: unknown, source: string) => void) | null = null; 30 + // Callback for broadcasting to renderer windows (bgWindow, tile windows, webview guests) 31 + let pubsubBroadcaster: ((topic: string, msg: unknown, source: string) => void) | null = null; 32 32 33 33 /** 34 34 * Pre-publish hooks let a module intercept publish calls before delivery. ··· 89 89 } 90 90 91 91 /** 92 - * Set the callback for broadcasting to extension windows 92 + * Set the callback for broadcasting to renderer windows 93 93 * This is called from the main process to inject the window broadcasting logic 94 94 */ 95 - export function setExtensionBroadcaster( 95 + export function setPubsubBroadcaster( 96 96 broadcaster: (topic: string, msg: unknown, source: string) => void 97 97 ): void { 98 - extensionBroadcaster = broadcaster; 98 + pubsubBroadcaster = broadcaster; 99 99 } 100 100 101 101 /** ··· 170 170 } 171 171 } 172 172 173 - // Route to extension windows (GLOBAL scope only) 174 - if (scope === scopes.GLOBAL && extensionBroadcaster) { 175 - extensionBroadcaster(topic, msg, source); 173 + // Route to renderer windows (GLOBAL scope only) 174 + if (scope === scopes.GLOBAL && pubsubBroadcaster) { 175 + pubsubBroadcaster(topic, msg, source); 176 176 } 177 177 } 178 178
+1 -1
backend/electron/test-fixture-glue.ts
··· 102 102 103 103 testFixtureWindow = win; 104 104 105 - // Register with the tile launcher so the main-process extensionBroadcaster 105 + // Register with the tile launcher so the main-process pubsubBroadcaster 106 106 // forwards pubsub messages to this window. Tests subscribe to topics 107 107 // (e.g. `editor:open`) and assert on the data received — without this 108 108 // registration, the broadcaster would skip the fixture and tests hang.
+2 -2
backend/electron/tile-launcher.ts
··· 57 57 // where electron's named ESM exports are empty. Unit tests load 58 58 // tile-launcher.js under that mode and would crash at module parse 59 59 // time. We inject the getters at app startup instead — same pattern 60 - // as `setExtensionBroadcaster` in pubsub.ts. Tests that stub the 60 + // as `setPubsubBroadcaster` in pubsub.ts. Tests that stub the 61 61 // launcher never call createTileBrowserWindow, so the hooks stay 62 62 // null and the baseline helpers fall back cleanly. 63 63 let _getProfileSession: (() => Electron.Session) | null = null; ··· 524 524 * page-glue call this helper after creating their BrowserWindow to: 525 525 * 526 526 * - Add the window to the `tileWindows` registry so the pubsub 527 - * extensionBroadcaster in main.ts forwards messages to it. 527 + * broadcaster in main.ts forwards messages to it. 528 528 * - Wire the same close/revoke cleanup the regular launcher uses. 529 529 * 530 530 * See docs/v1-removal-plan.md — Phase 1a/1b.
+1
docs/tasks.md
··· 51 51 52 52 - [x] **Commands for browser-extension options pages.** Registered `<name> options` command per installed Chromium extension that declares `options_page`/`options_ui.page`. 53 53 - [x] **Bundle TWP (Translate Web Pages) as a default browser extension.** TWP v10.1.1.0 vendored at `chrome-extensions/twp/` (~2.1MB). Auto-loads on startup. 54 + - [ ] **Restore bundled-extension links in page host widget.** Regression — at some point we lost the affordance where each bundled Chromium extension (TWP, Proton Pass, etc.) surfaced links in the page host widget to open its popup / options page / extension UI directly from the page canvas. Commands for options pages still exist (see above) but the in-widget links are gone. Investigate when this was lost (git log on the page widget / extension-host widget), restore the UI, and confirm it works for all bundled extensions. 54 55 55 56 --- 56 57