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 6 — delete scope (SELF/SYSTEM/GLOBAL)

+703 -814
+15 -15
app/cmd/background.js
··· 265 265 commandRegistry.set(cmd.name, entry); 266 266 liveRegisteredCommands.add(cmd.name); 267 267 } 268 - }, api.scopes.GLOBAL); 268 + }); 269 269 270 270 // Handle individual command registrations from extensions 271 271 // Merge-preserve pattern: see cmd:register-batch above for rationale. ··· 289 289 if (msg._nounCapability || existing._nounCapability) entry._nounCapability = msg._nounCapability || existing._nounCapability; 290 290 commandRegistry.set(msg.name, entry); 291 291 liveRegisteredCommands.add(msg.name); 292 - }, api.scopes.GLOBAL); 292 + }); 293 293 294 294 // Handle command unregistrations 295 295 api.pubsub.subscribe('cmd:unregister', (msg) => { 296 296 log('ext:cmd', 'cmd:unregister received:', msg.name); 297 297 commandRegistry.delete(msg.name); 298 - }, api.scopes.GLOBAL); 298 + }); 299 299 300 300 // ===== Noun Registration Handlers ===== 301 301 ··· 330 330 331 331 // Broadcast generated commands to panel via existing cmd:register-batch flow 332 332 if (generatedCommands.length > 0) { 333 - api.pubsub.publish('cmd:register-batch', { commands: generatedCommands }, api.scopes.GLOBAL); 333 + api.pubsub.publish('cmd:register-batch', { commands: generatedCommands }); 334 334 } 335 - }, api.scopes.GLOBAL); 335 + }); 336 336 337 337 // Handle noun unregistrations 338 338 api.pubsub.subscribe('noun:unregister', (msg) => { ··· 348 348 for (const cmd of commands) { 349 349 commandRegistry.delete(cmd.name); 350 350 liveRegisteredCommands.delete(cmd.name); 351 - api.pubsub.publish('cmd:unregister', { name: cmd.name }, api.scopes.GLOBAL); 351 + api.pubsub.publish('cmd:unregister', { name: cmd.name }); 352 352 } 353 353 354 354 nounRegistry.delete(msg.name); 355 - }, api.scopes.GLOBAL); 355 + }); 356 356 357 357 // Handle command list queries from the panel 358 358 api.pubsub.subscribe('cmd:query-commands', () => { 359 359 const commands = Array.from(commandRegistry.values()); 360 360 log('ext:cmd', 'cmd:query-commands received'); 361 - api.pubsub.publish('cmd:query-commands-response', { commands }, api.scopes.GLOBAL); 362 - }, api.scopes.GLOBAL); 361 + api.pubsub.publish('cmd:query-commands-response', { commands }); 362 + }); 363 363 364 364 log('ext:cmd', 'Command registry initialized'); 365 365 }; ··· 460 460 // main.ts skips local shortcut dispatch for Cmd+L on page host windows, 461 461 // so this only fires from non-page windows. 462 462 api.shortcuts.register(URL_MODE_SHORTCUT, () => { 463 - api.pubsub.publish('cmd:url-mode', {}, api.scopes.GLOBAL); 463 + api.pubsub.publish('cmd:url-mode', {}); 464 464 openPanelWindow(prefs); 465 465 }); 466 466 ··· 583 583 api.pubsub.subscribe('cmd:settings-changed', () => { 584 584 log('ext:cmd', 'settings changed, reinitializing'); 585 585 reinit(); 586 - }, api.scopes.GLOBAL); 586 + }); 587 587 588 588 // 4b. Save command cache after all extensions have loaded 589 589 api.pubsub.subscribe('ext:all-loaded', async () => { ··· 601 601 log('ext:cmd', 'Purging stale cached command:', name); 602 602 commandRegistry.delete(name); 603 603 // Notify panel so it removes the command from its local registry 604 - api.pubsub.publish('cmd:unregister', { name }, api.scopes.GLOBAL); 604 + api.pubsub.publish('cmd:unregister', { name }); 605 605 } 606 606 607 607 const { appVersion, extensionVersions } = await getCurrentVersions(); 608 608 await saveCommandCache(appVersion, extensionVersions); 609 609 }, 100); 610 - }, api.scopes.GLOBAL); 610 + }); 611 611 612 612 // Listen for settings updates from Settings UI 613 613 api.pubsub.subscribe('cmd:settings-update', async (msg) => { ··· 628 628 await saveSettings(currentSettings); 629 629 await reinit(); 630 630 631 - api.pubsub.publish('cmd:settings-changed', currentSettings, api.scopes.GLOBAL); 631 + api.pubsub.publish('cmd:settings-changed', currentSettings); 632 632 } catch (err) { 633 633 log.error('ext:cmd', 'settings-update error:', err); 634 634 } 635 - }, api.scopes.GLOBAL); 635 + }); 636 636 637 637 }; 638 638
+2 -2
app/cmd/chain-editor.js
··· 60 60 sessionId, 61 61 data, 62 62 mimeType 63 - }, api.scopes.GLOBAL); 63 + }); 64 64 } 65 65 66 66 /** ··· 186 186 187 187 console.log('[chain-editor] Received content, length:', (msg.data || '').length); 188 188 createEditor(msg.data || ''); 189 - }, api.scopes.GLOBAL); 189 + }); 190 190 } 191 191 192 192 init();
+9 -9
app/cmd/commands.js
··· 128 128 settled = true; 129 129 unsubscribe?.(); 130 130 resolve(result); 131 - }, api.scopes.GLOBAL); 131 + }); 132 132 133 133 api.publish(topic, { 134 134 ...ctx, 135 135 expectResult: true, 136 136 resultTopic 137 - }, api.scopes.GLOBAL); 137 + }); 138 138 139 139 setTimeout(() => { 140 140 if (settled) return; ··· 158 158 settled = true; 159 159 unsubscribe?.(); 160 160 resolve(result); 161 - }, api.scopes.GLOBAL); 161 + }); 162 162 163 163 api.publish(`cmd:execute:${cmdData.name}`, { 164 164 ...ctx, 165 165 expectResult: true, 166 166 resultTopic 167 - }, api.scopes.GLOBAL); 167 + }); 168 168 169 169 setTimeout(() => { 170 170 if (settled) return; ··· 191 191 }); 192 192 onCommandsUpdated(); // Single update after batch 193 193 } 194 - }, api.scopes.GLOBAL); 194 + }); 195 195 196 196 // Handle batch registrations from preload batching 197 197 api.subscribe('cmd:register-batch', (msg) => { ··· 203 203 }); 204 204 onCommandsUpdated(); // Single update after batch 205 205 } 206 - }, api.scopes.GLOBAL); 206 + }); 207 207 208 208 // Also listen for individual registrations while panel is open 209 209 api.subscribe('cmd:register', (msg) => { 210 210 debug && console.log('[cmd:commands] cmd:register received (live)', msg); 211 211 const command = createProxyCommand(msg); 212 212 addCommand(command); 213 - }, api.scopes.GLOBAL); 213 + }); 214 214 215 215 // Listen for unregistrations while panel is open 216 216 api.subscribe('cmd:unregister', (msg) => { 217 217 debug && console.log('[cmd:commands] cmd:unregister received', msg); 218 218 removeCommand(msg.name); 219 - }, api.scopes.GLOBAL); 219 + }); 220 220 221 221 // Query the background process for currently registered commands 222 222 debug && console.log('[cmd:commands] Querying for registered commands...'); 223 - api.publish('cmd:query-commands', {}, api.scopes.GLOBAL); 223 + api.publish('cmd:query-commands', {}); 224 224 225 225 debug && console.log('[cmd:commands] Command registration listeners initialized'); 226 226 }
+3 -3
app/cmd/commands/history.js
··· 215 215 } 216 216 }); 217 217 } 218 - }, api.scopes.GLOBAL); 218 + }); 219 219 220 220 // Handle URL deletions 221 221 api.subscribe('item:deleted', (msg) => { ··· 232 232 _registeredUrls.delete(url); 233 233 } 234 234 } 235 - }, api.scopes.GLOBAL); 235 + }); 236 236 237 237 // Re-sync after sync pull (history may have changed) 238 238 api.subscribe('sync:pull-completed', async () => { 239 239 debug && console.log('[history] sync pull completed, re-syncing history commands'); 240 240 await initializeSources(_addCommand, _batchApi); 241 - }, api.scopes.GLOBAL); 241 + }); 242 242 } 243 243 }; 244 244
+2 -2
app/cmd/commands/page.js
··· 38 38 } else { 39 39 resolve(msg.data); 40 40 } 41 - }, api.scopes.GLOBAL); 41 + }); 42 42 43 43 api.publish('page:cmd:request', { 44 44 requestId, 45 45 action, 46 46 ...params 47 - }, api.scopes.GLOBAL); 47 + }); 48 48 49 49 setTimeout(() => { 50 50 if (settled) return;
+1 -1
app/cmd/commands/url.js
··· 28 28 } 29 29 30 30 console.log('Saved URL:', result.data.id); 31 - api.publish('editor:changed', { action: 'add', itemId: result.data.id }, api.scopes.GLOBAL); 31 + api.publish('editor:changed', { action: 'add', itemId: result.data.id }); 32 32 return { success: true, message: 'URL saved' }; 33 33 } 34 34 }
+5 -5
app/cmd/nouns.js
··· 23 23 const batch = pending; 24 24 pending = []; 25 25 timer = null; 26 - api.publish('noun:register-batch', { nouns: batch }, api.scopes.GLOBAL); 26 + api.publish('noun:register-batch', { nouns: batch }); 27 27 } 28 28 29 29 /** ··· 63 63 try { 64 64 const result = await handler(msg); 65 65 if (msg.expectResult && msg.resultTopic) { 66 - api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 66 + api.publish(msg.resultTopic, result || { success: true }); 67 67 } 68 68 } catch (err) { 69 69 console.error('[nouns] Error in', cap, nounDef.name, err); 70 70 if (msg.expectResult && msg.resultTopic) { 71 - api.publish(msg.resultTopic, { error: err.message }, api.scopes.GLOBAL); 71 + api.publish(msg.resultTopic, { error: err.message }); 72 72 } 73 73 } 74 - }, api.scopes.GLOBAL); 74 + }); 75 75 } 76 76 77 77 // Queue metadata (booleans, not functions) for batched publish ··· 100 100 */ 101 101 export function unregisterNoun(name) { 102 102 delete nounHandlers[name]; 103 - api.publish('noun:unregister', { name }, api.scopes.GLOBAL); 103 + api.publish('noun:unregister', { name }); 104 104 } 105 105 106 106 /**
+9 -9
app/cmd/panel.js
··· 115 115 showUrlsInResults = payload.prefs.showUrlsInResults; 116 116 log('cmd:panel', 'showUrlsInResults updated:', showUrlsInResults); 117 117 } 118 - }, api.scopes.GLOBAL); 118 + }); 119 119 120 120 // Listen for URL mode activation (from Cmd+L shortcut in background.js) 121 121 api.subscribe('cmd:url-mode', () => { ··· 125 125 if (!document.hidden && !urlModeActive) { 126 126 enterUrlMode(); 127 127 } 128 - }, api.scopes.GLOBAL); 128 + }); 129 129 130 130 // Close the panel when the user focuses another Peek window. 131 131 // ··· 151 151 } catch (err) { 152 152 log.error('cmd:panel', 'Failed to hide panel on other-window focus:', err); 153 153 } 154 - }, api.scopes.GLOBAL); 154 + }); 155 155 156 156 // Chain popup state (module-level for cleanup) 157 157 let chainPopupWindowId = null; ··· 761 761 data: msg.data, 762 762 mimeType: msg.mimeType || mimeType, 763 763 }); 764 - }, api.scopes.GLOBAL); 764 + }); 765 765 766 766 const panelBounds = await api.window.getBounds(); 767 767 const editorHeight = 400; ··· 793 793 data, 794 794 mimeType, 795 795 title: title || 'Edit' 796 - }, api.scopes.GLOBAL); 796 + }); 797 797 } 798 798 799 799 // ===== Chain Steps UI ===== ··· 875 875 876 876 if (mimeType === 'item' && selectedItem.id) { 877 877 log('cmd:panel', 'Opening editor for selected item:', selectedItem.id); 878 - api.publish('editor:open', { itemId: selectedItem.id }, api.scopes.GLOBAL); 878 + api.publish('editor:open', { itemId: selectedItem.id }); 879 879 machine.setState(States.CLOSING); 880 880 setTimeout(shutdown, 100); 881 881 return; ··· 2352 2352 const outputMimeType = result.output.mimeType; 2353 2353 2354 2354 if (outputMimeType === 'new-item') { 2355 - api.publish('editor:add', { type: outputData.type || 'text' }, api.scopes.GLOBAL); 2355 + api.publish('editor:add', { type: outputData.type || 'text' }); 2356 2356 } else if (outputData.id) { 2357 - api.publish('editor:open', { itemId: outputData.id }, api.scopes.GLOBAL); 2357 + api.publish('editor:open', { itemId: outputData.id }); 2358 2358 if (result.output.isNew) { 2359 - api.publish('editor:changed', { action: 'add', itemId: outputData.id }, api.scopes.GLOBAL); 2359 + api.publish('editor:changed', { action: 'add', itemId: outputData.id }); 2360 2360 } 2361 2361 } 2362 2362 },
+1 -1
app/hud/background.js
··· 184 184 // windows are hidden/shown directly without pubsub round-trip. 185 185 api.pubsub.subscribe('app:focus-changed', (msg) => { 186 186 appFocused = !!msg.focused; 187 - }, api.scopes.GLOBAL); 187 + }); 188 188 189 189 // Open HUD if it was enabled in a previous session 190 190 if (hudEnabled) {
+1 -1
app/hud/widgets/izui.js
··· 46 46 currentIzuiState = msg.state; 47 47 updateDisplay(); 48 48 } 49 - }, api.scopes.GLOBAL); 49 + }); 50 50 51 51 // Periodic fallback 52 52 setInterval(refreshIzuiState, 5000);
+3 -3
app/hud/widgets/mode.js
··· 94 94 }); 95 95 96 96 // Refresh on window focus changes 97 - api.subscribe('window:focused', () => refreshMode(), api.scopes.GLOBAL); 98 - api.subscribe('window:opened', () => refreshMode(), api.scopes.GLOBAL); 99 - api.subscribe('window:closed', () => refreshMode(), api.scopes.GLOBAL); 97 + api.subscribe('window:focused', () => refreshMode()); 98 + api.subscribe('window:opened', () => refreshMode()); 99 + api.subscribe('window:closed', () => refreshMode()); 100 100 101 101 // Periodic fallback 102 102 setInterval(refreshMode, 5000);
+2 -2
app/hud/widgets/stats.js
··· 74 74 await refreshStats(); 75 75 76 76 // Refresh on window events 77 - api.subscribe('window:opened', () => refreshStats(), api.scopes.GLOBAL); 78 - api.subscribe('window:closed', () => refreshStats(), api.scopes.GLOBAL); 77 + api.subscribe('window:opened', () => refreshStats()); 78 + api.subscribe('window:closed', () => refreshStats()); 79 79 80 80 // Poll every 3 seconds for pubsub stats (they change frequently) 81 81 setInterval(refreshStats, 3000);
+3 -3
app/hud/widgets/window.js
··· 57 57 await refreshWindowInfo(); 58 58 59 59 // Refresh on window events 60 - api.subscribe('window:focused', () => refreshWindowInfo(), api.scopes.GLOBAL); 61 - api.subscribe('window:opened', () => refreshWindowInfo(), api.scopes.GLOBAL); 62 - api.subscribe('window:closed', () => refreshWindowInfo(), api.scopes.GLOBAL); 60 + api.subscribe('window:focused', () => refreshWindowInfo()); 61 + api.subscribe('window:opened', () => refreshWindowInfo()); 62 + api.subscribe('window:closed', () => refreshWindowInfo()); 63 63 64 64 // Periodic fallback 65 65 setInterval(refreshWindowInfo, 5000);
+9 -9
app/index.js
··· 70 70 // publish navigate event (createWindow may have just focused existing window) 71 71 if (section) { 72 72 setTimeout(() => { 73 - api.publish('settings:navigate', { section }, api.scopes.GLOBAL); 73 + api.publish('settings:navigate', { section }); 74 74 }, 100); 75 75 } 76 76 } catch (error) { ··· 615 615 } catch (err) { 616 616 log.error('core', 'registerExtensionCommands threw:', err); 617 617 } 618 - }, api.scopes.GLOBAL); 618 + }); 619 619 620 620 const p = prefs(); 621 621 ··· 623 623 api.publish(topicCorePrefs, { 624 624 id: id, 625 625 prefs: p 626 - }, api.scopes.SYSTEM); 626 + }); 627 627 628 628 log('timing', `core init: ${Date.now() - initStart}ms`); 629 629 ··· 681 681 if (win && win.id) { 682 682 // Small delay to ensure the page host has loaded and subscribed 683 683 setTimeout(() => { 684 - api.publish('page:show-navbar', { windowId: win.id, focusUrl: true }, api.scopes.GLOBAL); 684 + api.publish('page:show-navbar', { windowId: win.id, focusUrl: true }); 685 685 }, 200); 686 686 } 687 687 } catch (error) { ··· 709 709 api.subscribe('cmd:execute:reopen closed window', async (msg) => { 710 710 const result = await api.session.reopenLastClosed(); 711 711 if (msg.expectResult && msg.resultTopic) { 712 - api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 712 + api.publish(msg.resultTopic, result || { success: true }); 713 713 } 714 - }, api.scopes.GLOBAL); 714 + }); 715 715 api.publish('cmd:register', { 716 716 name: 'reopen closed window', 717 717 description: 'Reopen the last closed window (Cmd+Shift+T)', ··· 720 720 accepts: [], 721 721 produces: [], 722 722 params: [] 723 - }, api.scopes.GLOBAL); 723 + }); 724 724 725 725 // Signal to main process that frontend subscribers are ready. 726 726 // This unblocks pending external URLs that arrived during startup. ··· 774 774 api.subscribe('session:open-startup', () => { 775 775 log('core', 'Session restore declined/failed, opening startup feature'); 776 776 openStartupFeature(); 777 - }, api.scopes.GLOBAL); 777 + }); 778 778 779 779 // Feature enable/disable handler 780 780 // Extensions are now managed by main process ExtensionManager via IPC ··· 825 825 log('core', 'prefs changed, updating settings shortcut'); 826 826 initSettingsShortcut(msg.prefs); 827 827 } 828 - }, api.scopes.SYSTEM); 828 + }); 829 829 830 830 // Initialize core features (non-extension features only) 831 831 features().forEach(initFeature);
+8 -8
app/lib/tag-action-affordances.js
··· 281 281 rules = msg.data; 282 282 loaded = true; 283 283 } 284 - }, api.scopes.GLOBAL); 284 + }); 285 285 286 286 // Listen for changes (create/update/delete) 287 287 const unsubCreate = api.subscribe('tag-actions:create:response', () => { 288 - api.publish('tag-actions:get-all', {}, api.scopes.GLOBAL); 289 - }, api.scopes.GLOBAL); 288 + api.publish('tag-actions:get-all', {}); 289 + }); 290 290 291 291 const unsubUpdate = api.subscribe('tag-actions:update:response', () => { 292 - api.publish('tag-actions:get-all', {}, api.scopes.GLOBAL); 293 - }, api.scopes.GLOBAL); 292 + api.publish('tag-actions:get-all', {}); 293 + }); 294 294 295 295 const unsubDelete = api.subscribe('tag-actions:delete:response', () => { 296 - api.publish('tag-actions:get-all', {}, api.scopes.GLOBAL); 297 - }, api.scopes.GLOBAL); 296 + api.publish('tag-actions:get-all', {}); 297 + }); 298 298 299 299 // Initial fetch 300 - api.publish('tag-actions:get-all', {}, api.scopes.GLOBAL); 300 + api.publish('tag-actions:get-all', {}); 301 301 302 302 return { 303 303 getRules: () => rules,
+5 -5
app/page/page-notes.js
··· 69 69 }); 70 70 71 71 // Subscribe to relevant events 72 - api.subscribe('item:created', () => this._reload(), api.scopes.GLOBAL); 73 - api.subscribe('item:deleted', () => this._reload(), api.scopes.GLOBAL); 74 - api.subscribe('tag:item-added', () => this._reload(), api.scopes.GLOBAL); 75 - api.subscribe('tag:item-removed', () => this._reload(), api.scopes.GLOBAL); 76 - api.subscribe('sync:pull-completed', () => this._reload(), api.scopes.GLOBAL); 72 + api.subscribe('item:created', () => this._reload()); 73 + api.subscribe('item:deleted', () => this._reload()); 74 + api.subscribe('tag:item-added', () => this._reload()); 75 + api.subscribe('tag:item-removed', () => this._reload()); 76 + api.subscribe('sync:pull-completed', () => this._reload()); 77 77 } 78 78 79 79 /**
+9 -9
app/page/page-widgets.js
··· 78 78 // --- Subscription helpers --- 79 79 80 80 function subscribe(topic, handler) { 81 - const unsub = api.subscribe(topic, handler, api.scopes.GLOBAL); 81 + const unsub = api.subscribe(topic, handler); 82 82 unsubs.push(unsub); 83 83 return unsub; 84 84 } ··· 109 109 extensionId: msg.extensionId, 110 110 widgetId: msg.widgetId, 111 111 success: true, 112 - }, api.scopes.GLOBAL); 112 + }); 113 113 } 114 114 115 115 // --- Widget rendering --- ··· 142 142 api.publish('widget:closed', { 143 143 extensionId: msg.extensionId, 144 144 widgetId: msg.widgetId, 145 - }, api.scopes.GLOBAL); 145 + }); 146 146 }, 147 147 }); 148 148 ··· 198 198 extensionId: msg.extensionId, 199 199 error: 'No webview available', 200 200 success: false, 201 - }, api.scopes.GLOBAL); 201 + }); 202 202 return; 203 203 } 204 204 ··· 215 215 extensionId: msg.extensionId, 216 216 error: err.message || String(err), 217 217 success: false, 218 - }, api.scopes.GLOBAL); 218 + }); 219 219 return; 220 220 } 221 221 ··· 232 232 extensionId: msg.extensionId, 233 233 result, 234 234 success: true, 235 - }, api.scopes.GLOBAL); 235 + }); 236 236 }) 237 237 .catch((err) => { 238 238 api.publish('page:script-result', { ··· 240 240 extensionId: msg.extensionId, 241 241 error: err.message || String(err), 242 242 success: false, 243 - }, api.scopes.GLOBAL); 243 + }); 244 244 }); 245 245 } 246 246 247 247 // --- Lifecycle event publishers --- 248 248 249 249 function publishNavigated(url, title) { 250 - api.publish('page:navigated', { url, title }, api.scopes.GLOBAL); 250 + api.publish('page:navigated', { url, title }); 251 251 } 252 252 253 253 function publishWillClose(url) { 254 - api.publish('page:will-close', { url }, api.scopes.GLOBAL); 254 + api.publish('page:will-close', { url }); 255 255 } 256 256 257 257 // --- Init / Destroy ---
+21 -21
app/page/page.js
··· 1572 1572 console.log('[page] page:show-navbar received, windowId:', msg.windowId, 'myWindowId:', myWindowId); 1573 1573 if (msg.windowId != null && msg.windowId !== myWindowId) return; 1574 1574 show({ focusUrl: true, source: 'shortcut' }); 1575 - }, api.scopes.GLOBAL); 1575 + }); 1576 1576 1577 1577 // Cmd+R: reload the webview 1578 1578 api.subscribe('page:reload', (msg) => { 1579 1579 if (msg.windowId != null && msg.windowId !== myWindowId) return; 1580 1580 DEBUG && console.log('[page] Cmd+R: reloading webview'); 1581 1581 webview.reload(); 1582 - }, api.scopes.GLOBAL); 1582 + }); 1583 1583 1584 1584 // Cmd+[ / Cmd+Left: go back 1585 1585 api.subscribe('page:go-back', (msg) => { ··· 1589 1589 webview.goBack(); 1590 1590 updateState(); 1591 1591 } 1592 - }, api.scopes.GLOBAL); 1592 + }); 1593 1593 1594 1594 // Cmd+] / Cmd+Right: go forward 1595 1595 api.subscribe('page:go-forward', (msg) => { ··· 1599 1599 webview.goForward(); 1600 1600 updateState(); 1601 1601 } 1602 - }, api.scopes.GLOBAL); 1602 + }); 1603 1603 1604 1604 // Navigate to a new URL (used by pagestream to flip between cards). 1605 1605 // Tracks the original URL so Escape can skip accumulated history. ··· 1614 1614 webview.loadURL(url); 1615 1615 navbar.setUrl(url); 1616 1616 } 1617 - }, api.scopes.GLOBAL); 1617 + }); 1618 1618 1619 1619 // Forward card-navigation keys to pagestream (only fires when webview is NOT focused, 1620 1620 // e.g., after pressing Escape which blurs to the page chrome level) ··· 1638 1638 const action = NAV_MAP[e.key]; 1639 1639 if (action) { 1640 1640 e.preventDefault(); 1641 - api.publish('pagestream:nav', { action }, api.scopes.GLOBAL); 1641 + api.publish('pagestream:nav', { action }); 1642 1642 return; 1643 1643 } 1644 1644 ··· 1748 1748 api.subscribe('page:find', (msg) => { 1749 1749 if (msg.windowId != null && msg.windowId !== myWindowId) return; 1750 1750 showFindBar(); 1751 - }, api.scopes.GLOBAL); 1751 + }); 1752 1752 1753 1753 api.subscribe('page:find-next', (msg) => { 1754 1754 if (msg.windowId != null && msg.windowId !== myWindowId) return; ··· 1757 1757 } else { 1758 1758 showFindBar(); 1759 1759 } 1760 - }, api.scopes.GLOBAL); 1760 + }); 1761 1761 1762 1762 api.subscribe('page:find-prev', (msg) => { 1763 1763 if (msg.windowId != null && msg.windowId !== myWindowId) return; ··· 1766 1766 } else { 1767 1767 showFindBar(); 1768 1768 } 1769 - }, api.scopes.GLOBAL); 1769 + }); 1770 1770 1771 1771 // --- Escape handling --- 1772 1772 ··· 1977 1977 url: pageUrl, 1978 1978 title: pageTitle, 1979 1979 opensearchUrl 1980 - }, api.scopes.GLOBAL); 1980 + }); 1981 1981 1982 1982 DEBUG && console.log('[page] page:loaded published:', pageUrl, opensearchUrl ? `(OpenSearch: ${opensearchUrl})` : ''); 1983 1983 ··· 2081 2081 api.subscribe('page:maximize', (msg) => { 2082 2082 if (msg.windowId != null && msg.windowId !== myWindowId) return; 2083 2083 toggleMaximize(); 2084 - }, api.scopes.GLOBAL); 2084 + }); 2085 2085 2086 2086 // Navbar double-click to toggle maximize 2087 2087 navbar.addEventListener('dblclick', (e) => { ··· 2411 2411 content: contentEl, 2412 2412 autoDismiss: 8000, 2413 2413 }); 2414 - }, api.scopes.GLOBAL); 2414 + }); 2415 2415 2416 2416 // --- Popup-to-opener postMessage bridge: receiver side (Level 3) --- 2417 2417 // When main process routes a postMessage from a popup to this opener window, ··· 2736 2736 } catch { 2737 2737 // webview not ready 2738 2738 } 2739 - }, api.scopes.GLOBAL); 2739 + }); 2740 2740 2741 2741 // Also listen for get-for-url responses 2742 2742 api.subscribe('entities:get-for-url:response', (msg) => { ··· 2760 2760 } catch { 2761 2761 // webview not ready 2762 2762 } 2763 - }, api.scopes.GLOBAL); 2763 + }); 2764 2764 2765 2765 // Rescan button — triggers re-extraction of entities for current page 2766 2766 if (entitiesRescanBtn) { ··· 2780 2780 } 2781 2781 2782 2782 // Request re-extraction from entities extension 2783 - api.publish('entities:extract', { url: currentUrl }, api.scopes.GLOBAL); 2783 + api.publish('entities:extract', { url: currentUrl }); 2784 2784 }); 2785 2785 } 2786 2786 ··· 2794 2794 2795 2795 // Request entities for this URL from the entities extension 2796 2796 if (e.url && e.url.startsWith('http')) { 2797 - api.publish('entities:get-for-url', { url: e.url }, api.scopes.GLOBAL); 2797 + api.publish('entities:get-for-url', { url: e.url }); 2798 2798 } 2799 2799 2800 2800 // Load notes for the new URL ··· 3164 3164 } catch { /* ignore */ } 3165 3165 } 3166 3166 loadAllTags(); 3167 - }, api.scopes.GLOBAL); 3167 + }); 3168 3168 3169 3169 api.subscribe('tag:item-removed', async (msg) => { 3170 3170 if (msg && msg.itemId === currentItemId) { ··· 3180 3180 } 3181 3181 } catch { /* ignore */ } 3182 3182 } 3183 - }, api.scopes.GLOBAL); 3183 + }); 3184 3184 3185 3185 api.subscribe('tag:created', () => { 3186 3186 loadAllTags(); 3187 - }, api.scopes.GLOBAL); 3187 + }); 3188 3188 3189 3189 // --- Load tags on navigation --- 3190 3190 ··· 3249 3249 requestId, 3250 3250 data: data || null, 3251 3251 error: error || null 3252 - }, api.scopes.GLOBAL); 3252 + }); 3253 3253 }; 3254 3254 3255 3255 try { ··· 3580 3580 console.error('[page] page:cmd:request error:', err); 3581 3581 respond(null, err.message || 'Failed to execute page command'); 3582 3582 } 3583 - }, api.scopes.GLOBAL); 3583 + }); 3584 3584 3585 3585 // --- Extensions Panel: show loaded chrome extensions and adblocker --- 3586 3586
+4 -4
app/settings/settings.js
··· 222 222 await store.set(storageKeys.PREFS, prefs); 223 223 await store.set(storageKeys.ITEMS, features); 224 224 // Notify main process of prefs change for live updates (quit shortcut, dock visibility, etc.) 225 - api.publish('topic:core:prefs', { id, prefs }, api.scopes.SYSTEM); 225 + api.publish('topic:core:prefs', { id, prefs }); 226 226 }; 227 227 228 228 const pProps = schemas.prefs.properties; ··· 2279 2279 } 2280 2280 2281 2281 // Notify feature to hot-reload with new settings (GLOBAL for cross-process) 2282 - api.publish(settingsChangedTopic, {}, api.scopes.GLOBAL); 2282 + api.publish(settingsChangedTopic, {}); 2283 2283 }; 2284 2284 2285 2285 // Preferences ··· 3279 3279 if (window._refreshExtensionsList) { 3280 3280 window._refreshExtensionsList(); 3281 3281 } 3282 - }, api.scopes.GLOBAL); 3282 + }); 3283 3283 3284 3284 // Add Extensions section (bundled webextensions: ad blocker, consent-o-matic, etc.) 3285 3285 const privacyNav = document.createElement('a'); ··· 3499 3499 if (msg.section) { 3500 3500 showSection(msg.section); 3501 3501 } 3502 - }, api.scopes.GLOBAL); 3502 + }); 3503 3503 })(); 3504 3504 3505 3505 // Expose showSection for external navigation
+7 -7
backend/electron/datastore.test.ts
··· 278 278 it('should publish tag:created when a new tag is inserted', () => { 279 279 const events: Array<Record<string, unknown>> = []; 280 280 const sub = 'peek://test/tag-created'; 281 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:created', (msg) => { 281 + pubsub.subscribe(sub, 'tag:created', (msg) => { 282 282 events.push(msg as Record<string, unknown>); 283 283 }); 284 284 ··· 297 297 298 298 const events: Array<Record<string, unknown>> = []; 299 299 const sub = 'peek://test/tag-created-noop'; 300 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:created', (msg) => { 300 + pubsub.subscribe(sub, 'tag:created', (msg) => { 301 301 events.push(msg as Record<string, unknown>); 302 302 }); 303 303 ··· 313 313 314 314 const events: Array<Record<string, unknown>> = []; 315 315 const sub = 'peek://test/tag-renamed'; 316 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:renamed', (msg) => { 316 + pubsub.subscribe(sub, 'tag:renamed', (msg) => { 317 317 events.push(msg as Record<string, unknown>); 318 318 }); 319 319 ··· 330 330 it('should NOT publish tag:renamed when the tag id does not exist', () => { 331 331 const events: Array<Record<string, unknown>> = []; 332 332 const sub = 'peek://test/tag-renamed-miss'; 333 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:renamed', (msg) => { 333 + pubsub.subscribe(sub, 'tag:renamed', (msg) => { 334 334 events.push(msg as Record<string, unknown>); 335 335 }); 336 336 ··· 346 346 347 347 const events: Array<Record<string, unknown>> = []; 348 348 const sub = 'peek://test/tag-deleted'; 349 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:deleted', (msg) => { 349 + pubsub.subscribe(sub, 'tag:deleted', (msg) => { 350 350 events.push(msg as Record<string, unknown>); 351 351 }); 352 352 ··· 362 362 it('should NOT publish tag:deleted when no row was removed', () => { 363 363 const events: Array<Record<string, unknown>> = []; 364 364 const sub = 'peek://test/tag-deleted-miss'; 365 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:deleted', (msg) => { 365 + pubsub.subscribe(sub, 'tag:deleted', (msg) => { 366 366 events.push(msg as Record<string, unknown>); 367 367 }); 368 368 ··· 378 378 379 379 const events: Array<Record<string, unknown>> = []; 380 380 const sub = 'peek://test/tag-color'; 381 - pubsub.subscribe(sub, pubsub.scopes.GLOBAL, 'tag:color-changed', (msg) => { 381 + pubsub.subscribe(sub, 'tag:color-changed', (msg) => { 382 382 events.push(msg as Record<string, unknown>); 383 383 }); 384 384
+7 -7
backend/electron/datastore.ts
··· 32 32 import { DEBUG } from './config.js'; 33 33 import { DATASTORE_VERSION } from '../version.js'; 34 34 import { addDeviceMetadata } from './device.js'; 35 - import { publish, scopes as pubsubScopes, getSystemAddress } from './pubsub.js'; 35 + import { publish, getSystemAddress } from './pubsub.js'; 36 36 37 37 // Load canonical schema for validation 38 38 // Path is relative to compiled JS in dist/backend/electron/ ··· 2789 2789 // Emit tag:created so any caller (legacy ipc, strict tile handlers, direct 2790 2790 // imports) triggers downstream UI refreshes (groups, tags page, etc.). 2791 2791 try { 2792 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:created', { 2792 + publish(getSystemAddress(), 'tag:created', { 2793 2793 tagId: newTag.id, 2794 2794 name: newTag.name, 2795 2795 // Back-compat alias used by existing listeners ··· 2818 2818 const updated = getDb().prepare('SELECT * FROM tags WHERE id = ?').get(id) as Tag; 2819 2819 2820 2820 try { 2821 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:renamed', { 2821 + publish(getSystemAddress(), 'tag:renamed', { 2822 2822 tagId: id, 2823 2823 oldName: existing?.name, 2824 2824 newName: updated.name, ··· 2841 2841 const updated = getDb().prepare('SELECT * FROM tags WHERE id = ?').get(id) as Tag; 2842 2842 2843 2843 try { 2844 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:color-changed', { 2844 + publish(getSystemAddress(), 'tag:color-changed', { 2845 2845 tagId: id, 2846 2846 color, 2847 2847 }); ··· 2866 2866 2867 2867 if (deleted) { 2868 2868 try { 2869 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:deleted', { 2869 + publish(getSystemAddress(), 'tag:deleted', { 2870 2870 tagId: id, 2871 2871 name: existing?.name, 2872 2872 // Back-compat alias ··· 3064 3064 if (!alreadyExists) { 3065 3065 try { 3066 3066 const item = getDb().prepare('SELECT type FROM items WHERE id = ?').get(itemId) as { type: string } | undefined; 3067 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:item-added', { 3067 + publish(getSystemAddress(), 'tag:item-added', { 3068 3068 tagId: tag.id, 3069 3069 tagName: tag.name, 3070 3070 itemId, ··· 3704 3704 const { alreadyExists } = tagItem(itemId, historyTag.id); 3705 3705 if (!alreadyExists) { 3706 3706 try { 3707 - publish(getSystemAddress(), pubsubScopes.GLOBAL, 'tag:item-added', { 3707 + publish(getSystemAddress(), 'tag:item-added', { 3708 3708 tagId: historyTag.id, 3709 3709 tagName: historyTag.name, 3710 3710 itemId,
+7 -8
backend/electron/entry.ts
··· 46 46 unregisterLocalShortcut, 47 47 handleLocalShortcut, 48 48 // PubSub 49 - scopes, 50 49 publish, 51 50 subscribe, 52 51 getSystemAddress, ··· 464 463 // Publish :result back immediately so the cmd panel proxy resolves instead 465 464 // of waiting for its 30s timeout (openChromeExtensionPage is fire-and-forget 466 465 // from the cmd panel's perspective). 467 - subscribe(systemAddress, scopes.GLOBAL, execTopic, () => { 466 + subscribe(systemAddress, execTopic, () => { 468 467 DEBUG && console.log(`[chrome-ext] Executing command: ${cmd.commandName}`); 469 468 openChromeExtensionPage(cmd.extensionId, 'options'); 470 - publish(systemAddress, scopes.GLOBAL, `${execTopic}:result`, { success: true }); 469 + publish(systemAddress, `${execTopic}:result`, { success: true }); 471 470 }); 472 471 473 472 // Register the command with the cmd registry. 474 - publish(systemAddress, scopes.GLOBAL, 'cmd:register', { 473 + publish(systemAddress, 'cmd:register', { 475 474 name: cmd.commandName, 476 475 description: `Open ${cmd.extensionName} options page`, 477 476 source: systemAddress, ··· 715 714 716 715 // listen for app prefs to configure ourself 717 716 // TODO: kinda janky, needs rethink 718 - subscribe(systemAddress, scopes.SYSTEM, strings.topics.prefs, async (msg: unknown) => { 717 + subscribe(systemAddress, strings.topics.prefs, async (msg: unknown) => { 719 718 const prefsMsg = msg as { prefs: Record<string, unknown> }; 720 719 DEBUG && console.log('PREFS', prefsMsg); 721 720 ··· 731 730 initTray(ROOT_DIR, { 732 731 tooltip: labels.tray.tooltip, 733 732 onClick: () => { 734 - publish(WEB_CORE_ADDRESS, scopes.GLOBAL, 'open', { 733 + publish(WEB_CORE_ADDRESS, 'open', { 735 734 address: SETTINGS_ADDRESS 736 735 }); 737 736 } ··· 792 791 // Nothing restored (user declined, or snapshot was empty). 793 792 // The startup feature was suppressed, so open it now. 794 793 DEBUG && console.log('[session] No windows restored, opening startup feature'); 795 - publish(systemAddress, scopes.GLOBAL, 'session:open-startup', {}); 794 + publish(systemAddress, 'session:open-startup', {}); 796 795 } 797 796 } catch (err) { 798 797 console.error('[session] Session restore failed:', err); 799 798 _sessionRestorePending = false; 800 - publish(systemAddress, scopes.GLOBAL, 'session:open-startup', {}); 799 + publish(systemAddress, 'session:open-startup', {}); 801 800 } 802 801 // Signal that session restore is done (success or failure) 803 802 // so pending external URLs can now be processed.
-3
backend/electron/index.ts
··· 126 126 127 127 // PubSub messaging 128 128 export { 129 - scopes, 130 129 publish, 131 130 subscribe, 132 131 unsubscribe, ··· 135 134 getSystemAddress, 136 135 } from './pubsub.js'; 137 136 138 - export type { Scope } from './pubsub.js'; 139 137 140 138 // Main process orchestration 141 139 export { ··· 229 227 export type { 230 228 IPeekApi, 231 229 ApiResult, 232 - ApiScope, 233 230 IShortcutsApi, 234 231 IWindowApi, 235 232 IDatastoreApi,
+45 -46
backend/electron/ipc.ts
··· 115 115 import { 116 116 publish, 117 117 subscribe, 118 - scopes as PubSubScopes, 119 118 getSystemAddress, 120 119 getStats as getPubSubStats, 121 120 } from './pubsub.js'; ··· 855 854 if (entry.scrollY != null) options.scrollY = entry.scrollY; 856 855 857 856 // Publish event for the background window to pick up and open 858 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'window:reopen-request', { 857 + publish(getSystemAddress(), 'window:reopen-request', { 859 858 url: entry.url, 860 859 options, 861 860 }); ··· 893 892 } 894 893 895 894 // Publish event for the background window to open the URL 896 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'window:new-page-request', { 895 + publish(getSystemAddress(), 'window:new-page-request', { 897 896 url: 'peek://app/new-tab/index.html', 898 897 options, 899 898 }); ··· 997 996 // Reload the page so content is fresh (the webview may have navigated 998 997 // away or the content may be stale from session restore). 999 998 try { 1000 - publish('system', PubSubScopes.GLOBAL, 'page:reload', { windowId: existingByUrl.id }); 999 + publish('system', 'page:reload', { windowId: existingByUrl.id }); 1001 1000 } catch (e) { 1002 1001 DEBUG && console.log('Failed to send reload to reused window:', e); 1003 1002 } ··· 1652 1651 }); 1653 1652 popupItemId = popupTrack.itemId; 1654 1653 if (popupTrack.created) { 1655 - publish('system', PubSubScopes.GLOBAL, 'item:created', { 1654 + publish('system', 'item:created', { 1656 1655 itemId: popupTrack.itemId, 1657 1656 itemType: 'url', 1658 1657 content: popupUrl, ··· 1798 1797 const modifier = process.platform === 'darwin' ? input.meta : input.control; 1799 1798 if (input.type !== 'keyDown' || !modifier) return; 1800 1799 if (input.key === 'l') { 1801 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:show-navbar', { windowId: popupWin.id }); 1800 + publish('peek://system/', 'page:show-navbar', { windowId: popupWin.id }); 1802 1801 event.preventDefault(); 1803 1802 } else if (input.key === 'r') { 1804 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:reload', { windowId: popupWin.id }); 1803 + publish('peek://system/', 'page:reload', { windowId: popupWin.id }); 1805 1804 event.preventDefault(); 1806 1805 } else if (input.key === 'f') { 1807 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find', { windowId: popupWin.id }); 1806 + publish('peek://system/', 'page:find', { windowId: popupWin.id }); 1808 1807 event.preventDefault(); 1809 1808 } else if (input.key === 'g' && input.shift) { 1810 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-prev', { windowId: popupWin.id }); 1809 + publish('peek://system/', 'page:find-prev', { windowId: popupWin.id }); 1811 1810 event.preventDefault(); 1812 1811 } else if (input.key === 'g') { 1813 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-next', { windowId: popupWin.id }); 1812 + publish('peek://system/', 'page:find-next', { windowId: popupWin.id }); 1814 1813 event.preventDefault(); 1815 1814 } else if (input.key === '[' || (input.key === 'ArrowLeft' && !input.shift)) { 1816 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-back', { windowId: popupWin.id }); 1815 + publish('peek://system/', 'page:go-back', { windowId: popupWin.id }); 1817 1816 event.preventDefault(); 1818 1817 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 1819 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-forward', { windowId: popupWin.id }); 1818 + publish('peek://system/', 'page:go-forward', { windowId: popupWin.id }); 1820 1819 event.preventDefault(); 1821 1820 } 1822 1821 }); ··· 1831 1830 } catch (e) { 1832 1831 DEBUG && console.log('Failed to update title from popup guest page-title-updated:', e); 1833 1832 } 1834 - publish('system', PubSubScopes.GLOBAL, 'page:content-ready', { 1833 + publish('system', 'page:content-ready', { 1835 1834 url: guestUrl, 1836 1835 title, 1837 1836 windowId: popupWin.id, ··· 1942 1941 const modifier = process.platform === 'darwin' ? input.meta : input.control; 1943 1942 if (input.type !== 'keyDown' || !modifier) return; 1944 1943 if (input.key === 'l') { 1945 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:show-navbar', { windowId: popupWin.id }); 1944 + publish('peek://system/', 'page:show-navbar', { windowId: popupWin.id }); 1946 1945 event.preventDefault(); 1947 1946 } else if (input.key === 'r') { 1948 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:reload', { windowId: popupWin.id }); 1947 + publish('peek://system/', 'page:reload', { windowId: popupWin.id }); 1949 1948 event.preventDefault(); 1950 1949 } else if (input.key === 'f') { 1951 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find', { windowId: popupWin.id }); 1950 + publish('peek://system/', 'page:find', { windowId: popupWin.id }); 1952 1951 event.preventDefault(); 1953 1952 } else if (input.key === 'g' && input.shift) { 1954 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-prev', { windowId: popupWin.id }); 1953 + publish('peek://system/', 'page:find-prev', { windowId: popupWin.id }); 1955 1954 event.preventDefault(); 1956 1955 } else if (input.key === 'g') { 1957 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-next', { windowId: popupWin.id }); 1956 + publish('peek://system/', 'page:find-next', { windowId: popupWin.id }); 1958 1957 event.preventDefault(); 1959 1958 } else if (input.key === '[' || (input.key === 'ArrowLeft' && !input.shift)) { 1960 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-back', { windowId: popupWin.id }); 1959 + publish('peek://system/', 'page:go-back', { windowId: popupWin.id }); 1961 1960 event.preventDefault(); 1962 1961 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 1963 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-forward', { windowId: popupWin.id }); 1962 + publish('peek://system/', 'page:go-forward', { windowId: popupWin.id }); 1964 1963 event.preventDefault(); 1965 1964 } 1966 1965 }); ··· 1997 1996 const modifier = process.platform === 'darwin' ? input.meta : input.control; 1998 1997 if (input.type !== 'keyDown' || !modifier) return; 1999 1998 if (input.key === 'l') { 2000 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:show-navbar', { windowId: win.id }); 1999 + publish('peek://system/', 'page:show-navbar', { windowId: win.id }); 2001 2000 event.preventDefault(); 2002 2001 } else if (input.key === 'r') { 2003 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:reload', { windowId: win.id }); 2002 + publish('peek://system/', 'page:reload', { windowId: win.id }); 2004 2003 event.preventDefault(); 2005 2004 } else if (input.key === 'f') { 2006 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find', { windowId: win.id }); 2005 + publish('peek://system/', 'page:find', { windowId: win.id }); 2007 2006 event.preventDefault(); 2008 2007 } else if (input.key === 'g' && input.shift) { 2009 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-prev', { windowId: win.id }); 2008 + publish('peek://system/', 'page:find-prev', { windowId: win.id }); 2010 2009 event.preventDefault(); 2011 2010 } else if (input.key === 'g') { 2012 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-next', { windowId: win.id }); 2011 + publish('peek://system/', 'page:find-next', { windowId: win.id }); 2013 2012 event.preventDefault(); 2014 2013 } else if (input.key === '[' || (input.key === 'ArrowLeft' && !input.shift)) { 2015 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-back', { windowId: win.id }); 2014 + publish('peek://system/', 'page:go-back', { windowId: win.id }); 2016 2015 event.preventDefault(); 2017 2016 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 2018 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-forward', { windowId: win.id }); 2017 + publish('peek://system/', 'page:go-forward', { windowId: win.id }); 2019 2018 event.preventDefault(); 2020 2019 } 2021 2020 }); ··· 2034 2033 DEBUG && console.log('Failed to update title from guest page-title-updated:', e); 2035 2034 } 2036 2035 // Notify extensions that page content is available for extraction 2037 - publish('system', PubSubScopes.GLOBAL, 'page:content-ready', { 2036 + publish('system', 'page:content-ready', { 2038 2037 url: guestUrl, 2039 2038 title, 2040 2039 windowId: win.id, ··· 2175 2174 const modifier = process.platform === 'darwin' ? input.meta : input.control; 2176 2175 if (input.type !== 'keyDown' || !modifier) return; 2177 2176 if (input.key === 'l') { 2178 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:show-navbar', { windowId: win.id }); 2177 + publish('peek://system/', 'page:show-navbar', { windowId: win.id }); 2179 2178 event.preventDefault(); 2180 2179 } else if (input.key === 'r') { 2181 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:reload', { windowId: win.id }); 2180 + publish('peek://system/', 'page:reload', { windowId: win.id }); 2182 2181 event.preventDefault(); 2183 2182 } else if (input.key === 'f') { 2184 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find', { windowId: win.id }); 2183 + publish('peek://system/', 'page:find', { windowId: win.id }); 2185 2184 event.preventDefault(); 2186 2185 } else if (input.key === 'g' && input.shift) { 2187 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-prev', { windowId: win.id }); 2186 + publish('peek://system/', 'page:find-prev', { windowId: win.id }); 2188 2187 event.preventDefault(); 2189 2188 } else if (input.key === 'g') { 2190 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:find-next', { windowId: win.id }); 2189 + publish('peek://system/', 'page:find-next', { windowId: win.id }); 2191 2190 event.preventDefault(); 2192 2191 } else if (input.key === '[' || (input.key === 'ArrowLeft' && !input.shift)) { 2193 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-back', { windowId: win.id }); 2192 + publish('peek://system/', 'page:go-back', { windowId: win.id }); 2194 2193 event.preventDefault(); 2195 2194 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 2196 - publish('peek://system/', PubSubScopes.GLOBAL, 'page:go-forward', { windowId: win.id }); 2195 + publish('peek://system/', 'page:go-forward', { windowId: win.id }); 2197 2196 event.preventDefault(); 2198 2197 } 2199 2198 }); ··· 2290 2289 DEBUG && console.log('Failed to update title from page-title-updated:', e); 2291 2290 } 2292 2291 // Notify extensions that page content is available for extraction 2293 - publish('system', PubSubScopes.GLOBAL, 'page:content-ready', { 2292 + publish('system', 'page:content-ready', { 2294 2293 url: pageUrl, 2295 2294 title, 2296 2295 windowId: win.id, ··· 2340 2339 title: options.title || (win.getTitle() !== 'Loading...' ? win.getTitle() : '') || '', 2341 2340 }); 2342 2341 if (trackResult.created) { 2343 - publish('system', PubSubScopes.GLOBAL, 'item:created', { 2342 + publish('system', 'item:created', { 2344 2343 itemId: trackResult.itemId, 2345 2344 itemType: 'url', 2346 2345 content: url, ··· 2377 2376 title: (win.getTitle() !== 'Loading...' ? win.getTitle() : '') || '', 2378 2377 }); 2379 2378 if (navTrack.created) { 2380 - publish('system', PubSubScopes.GLOBAL, 'item:created', { 2379 + publish('system', 'item:created', { 2381 2380 itemId: navTrack.itemId, 2382 2381 itemType: 'url', 2383 2382 content: navUrl, ··· 2410 2409 // Include `role` so subscribers (e.g. cmd panel's auto-hide-on- 2411 2410 // other-window-focus) can filter out utility children (chain 2412 2411 // popup, etc.) without another round-trip. 2413 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'window:focused', { 2412 + publish(getSystemAddress(), 'window:focused', { 2414 2413 id: win.id, 2415 2414 role: winInfo?.params?.role as string | undefined, 2416 2415 }); ··· 2517 2516 } 2518 2517 2519 2518 // Notify HUD and other listeners that a window was opened 2520 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'window:opened', { 2519 + publish(getSystemAddress(), 'window:opened', { 2521 2520 id: win.id, 2522 2521 url, 2523 2522 source: msg.source ··· 3271 3270 // PubSub publish 3272 3271 ipcMain.on(IPC_CHANNELS.PUBLISH, (_ev, msg) => { 3273 3272 DEBUG && console.log('ipc:publish', msg); 3274 - publish(msg.source, msg.scope, msg.topic, msg.data); 3273 + publish(msg.source, msg.topic, msg.data); 3275 3274 }); 3276 3275 3277 3276 // PubSub subscribe 3278 3277 ipcMain.on(IPC_CHANNELS.SUBSCRIBE, (ev, msg) => { 3279 3278 DEBUG && console.log('ipc:subscribe', msg); 3280 3279 3281 - subscribe(msg.source, msg.scope, msg.topic, (data: unknown) => { 3280 + subscribe(msg.source, msg.topic, (data: unknown) => { 3282 3281 DEBUG && console.log('ipc:subscribe:notification', msg); 3283 3282 ev.reply(msg.replyTopic, data); 3284 3283 }); ··· 3562 3561 */ 3563 3562 export function registerSyncHandlers(): void { 3564 3563 // Handle declarative sync trigger (from manifest-declared commands) 3565 - subscribe(getSystemAddress(), PubSubScopes.GLOBAL, 'sync:manual-trigger', async () => { 3564 + subscribe(getSystemAddress(), 'sync:manual-trigger', async () => { 3566 3565 DEBUG && console.log('[sync] Manual sync triggered via declarative command'); 3567 3566 try { 3568 3567 const config = getSyncConfig(); ··· 3575 3574 3576 3575 // Notify all windows that sync pulled new data so they can refresh 3577 3576 if (result.pulled > 0) { 3578 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'sync:pull-completed', { 3577 + publish(getSystemAddress(), 'sync:pull-completed', { 3579 3578 pulled: result.pulled, 3580 3579 pushed: result.pushed, 3581 3580 conflicts: result.conflicts ··· 3629 3628 3630 3629 // Notify all windows that sync pulled new data so they can refresh 3631 3630 if (result.pulled > 0) { 3632 - publish('system', PubSubScopes.GLOBAL, 'sync:pull-completed', { 3631 + publish('system', 'sync:pull-completed', { 3633 3632 pulled: result.pulled, 3634 3633 conflicts: result.conflicts 3635 3634 }); ··· 3888 3887 addContextEntry('mode', data.mode, { windowId, source }); 3889 3888 3890 3889 // Publish mode change for backwards compatibility 3891 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'modes:changed', { 3890 + publish(getSystemAddress(), 'modes:changed', { 3892 3891 windowId, 3893 3892 major: data.mode, 3894 3893 });
+26 -26
backend/electron/lazy-loading.test.ts
··· 27 27 let pendingMsg: unknown = null; 28 28 29 29 // Simulate the lazy stub 30 - pubsub.subscribe(stubSource, pubsub.scopes.GLOBAL, topic, async (msg: unknown) => { 30 + pubsub.subscribe(stubSource, topic, async (msg: unknown) => { 31 31 pendingMsg = msg; 32 32 loadTriggered = true; 33 33 ··· 38 38 pubsub.unsubscribe(stubSource, topic); 39 39 40 40 // Subscribe the "real" handler 41 - pubsub.subscribe('peek://test/', pubsub.scopes.GLOBAL, topic, (m: unknown) => { 41 + pubsub.subscribe('peek://test/', topic, (m: unknown) => { 42 42 replayed = m; 43 43 }); 44 44 45 45 // Replay buffered message 46 - pubsub.publish(stubSource, pubsub.scopes.GLOBAL, topic, pendingMsg); 46 + pubsub.publish(stubSource, topic, pendingMsg); 47 47 }); 48 48 49 49 // Invoke the command 50 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { arg: 'hello' }); 50 + pubsub.publish('peek://cmd/', topic, { arg: 'hello' }); 51 51 assert.strictEqual(loadTriggered, true); 52 52 53 53 // Wait for async replay ··· 72 72 let resolveLoad: () => void; 73 73 const loadPromise = new Promise<void>(r => { resolveLoad = r; }); 74 74 75 - pubsub.subscribe(stubSource, pubsub.scopes.GLOBAL, topic, async (msg: unknown) => { 75 + pubsub.subscribe(stubSource, topic, async (msg: unknown) => { 76 76 const wasLoading = alreadyLoading; 77 77 pendingMsg = msg; 78 78 ··· 90 90 pubsub.unsubscribe(stubSource, topic); 91 91 92 92 // Subscribe the "real" handler 93 - pubsub.subscribe('peek://test2/', pubsub.scopes.GLOBAL, topic, (m: unknown) => { 93 + pubsub.subscribe('peek://test2/', topic, (m: unknown) => { 94 94 replayed = m; 95 95 }); 96 96 97 97 // Replay buffered message (latest wins) 98 - pubsub.publish(stubSource, pubsub.scopes.GLOBAL, topic, pendingMsg); 98 + pubsub.publish(stubSource, topic, pendingMsg); 99 99 }); 100 100 101 101 // First invocation 102 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { version: 1 }); 102 + pubsub.publish('peek://cmd/', topic, { version: 1 }); 103 103 // Second invocation while loading 104 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { version: 2 }); 104 + pubsub.publish('peek://cmd/', topic, { version: 2 }); 105 105 106 106 assert.strictEqual(loadCount, 1, 'Should only trigger load once'); 107 107 ··· 121 121 const stub2 = 'lazy-stub/cmd-b'; 122 122 let a = false, b = false; 123 123 124 - pubsub.subscribe(stub1, pubsub.scopes.GLOBAL, 'cmd:execute:cmd-a', () => { a = true; }); 125 - pubsub.subscribe(stub2, pubsub.scopes.GLOBAL, 'cmd:execute:cmd-b', () => { b = true; }); 124 + pubsub.subscribe(stub1, 'cmd:execute:cmd-a', () => { a = true; }); 125 + pubsub.subscribe(stub2, 'cmd:execute:cmd-b', () => { b = true; }); 126 126 127 127 // Unsubscribing one shouldn't affect the other 128 128 pubsub.unsubscribe(stub1, 'cmd:execute:cmd-a'); 129 129 130 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, 'cmd:execute:cmd-b', {}); 130 + pubsub.publish('peek://cmd/', 'cmd:execute:cmd-b', {}); 131 131 assert.strictEqual(b, true, 'cmd-b stub should still work'); 132 132 133 133 pubsub.unsubscribe(stub2, 'cmd:execute:cmd-b'); ··· 141 141 let replayed: unknown = null; 142 142 let pendingMsg: unknown = null; 143 143 144 - pubsub.subscribe(interceptorSource, pubsub.scopes.GLOBAL, topic, async (msg: unknown) => { 144 + pubsub.subscribe(interceptorSource, topic, async (msg: unknown) => { 145 145 pendingMsg = msg; 146 146 147 147 // Simulate async load ··· 150 150 pubsub.unsubscribe(interceptorSource, topic); 151 151 152 152 // Subscribe the "real" handler 153 - pubsub.subscribe('peek://editor/', pubsub.scopes.GLOBAL, topic, (m: unknown) => { 153 + pubsub.subscribe('peek://editor/', topic, (m: unknown) => { 154 154 replayed = m; 155 155 }); 156 156 157 - pubsub.publish(interceptorSource, pubsub.scopes.GLOBAL, topic, pendingMsg); 157 + pubsub.publish(interceptorSource, topic, pendingMsg); 158 158 }); 159 159 160 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { file: 'test.txt' }); 160 + pubsub.publish('peek://cmd/', topic, { file: 'test.txt' }); 161 161 162 162 await Promise.resolve(); 163 163 await Promise.resolve(); ··· 177 177 let resolveLoad: () => void; 178 178 const loadPromise = new Promise<void>(r => { resolveLoad = r; }); 179 179 180 - pubsub.subscribe(interceptorSource, pubsub.scopes.GLOBAL, topic, async (msg: unknown) => { 180 + pubsub.subscribe(interceptorSource, topic, async (msg: unknown) => { 181 181 const wasLoading = alreadyLoading; 182 182 pendingMsg = msg; 183 183 ··· 188 188 189 189 pubsub.unsubscribe(interceptorSource, topic); 190 190 191 - pubsub.subscribe('peek://editor2/', pubsub.scopes.GLOBAL, topic, (m: unknown) => { 191 + pubsub.subscribe('peek://editor2/', topic, (m: unknown) => { 192 192 replayed = m; 193 193 }); 194 194 195 - pubsub.publish(interceptorSource, pubsub.scopes.GLOBAL, topic, pendingMsg); 195 + pubsub.publish(interceptorSource, topic, pendingMsg); 196 196 }); 197 197 198 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { file: 'a.txt' }); 199 - pubsub.publish('peek://cmd/', pubsub.scopes.GLOBAL, topic, { file: 'b.txt' }); 198 + pubsub.publish('peek://cmd/', topic, { file: 'a.txt' }); 199 + pubsub.publish('peek://cmd/', topic, { file: 'b.txt' }); 200 200 201 201 resolveLoad!(); 202 202 await Promise.resolve(); ··· 212 212 const src2 = 'lazy-interceptor/topic-y'; 213 213 let x = false, y = false; 214 214 215 - pubsub.subscribe(src1, pubsub.scopes.GLOBAL, 'topic-x', () => { x = true; }); 216 - pubsub.subscribe(src2, pubsub.scopes.GLOBAL, 'topic-y', () => { y = true; }); 215 + pubsub.subscribe(src1, 'topic-x', () => { x = true; }); 216 + pubsub.subscribe(src2, 'topic-y', () => { y = true; }); 217 217 218 218 pubsub.unsubscribe(src1, 'topic-x'); 219 219 220 - pubsub.publish('peek://test/', pubsub.scopes.GLOBAL, 'topic-y', {}); 220 + pubsub.publish('peek://test/', 'topic-y', {}); 221 221 assert.strictEqual(y, true); 222 222 223 223 pubsub.unsubscribe(src2, 'topic-y'); ··· 228 228 it('should detect when all command subscribers are present', () => { 229 229 const topics = ['cmd:execute:a', 'cmd:execute:b', 'cmd:execute:c']; 230 230 for (const t of topics) { 231 - pubsub.subscribe(`peek://test/${t}`, pubsub.scopes.GLOBAL, t, () => {}); 231 + pubsub.subscribe(`peek://test/${t}`, t, () => {}); 232 232 } 233 233 234 234 const allPresent = topics.every(t => pubsub.hasSubscriber(t)); ··· 240 240 }); 241 241 242 242 it('should detect missing subscribers', () => { 243 - pubsub.subscribe('peek://test/x', pubsub.scopes.GLOBAL, 'cmd:execute:present', () => {}); 243 + pubsub.subscribe('peek://test/x', 'cmd:execute:present', () => {}); 244 244 245 245 assert.strictEqual(pubsub.hasSubscriber('cmd:execute:present'), true); 246 246 assert.strictEqual(pubsub.hasSubscriber('cmd:execute:missing'), false);
+26 -27
backend/electron/main.ts
··· 25 25 import { installLoadOnDispatchHook } from './tile-lazy.js'; 26 26 import { initTray } from './tray.js'; 27 27 import { registerLocalShortcut, unregisterLocalShortcut, handleLocalShortcut, registerGlobalShortcut, unregisterGlobalShortcut, unregisterShortcutsForAddress } from './shortcuts.js'; 28 - import { scopes, publish, subscribe, unsubscribe, hasSubscriber, setPubsubBroadcaster, getSystemAddress } from './pubsub.js'; 28 + import { publish, subscribe, unsubscribe, hasSubscriber, setPubsubBroadcaster, getSystemAddress } from './pubsub.js'; 29 29 import { WEB_CORE_ADDRESS, isTestProfile, isDevProfile, isEphemeralProfile, isHeadless, getProfile, setTilePreloadPath, getTilePreloadPath, DEBUG } from './config.js'; 30 30 import { getSystemThemeBackgroundColor } from './windows.js'; 31 31 import { getProfileSession, getPartitionString, getCurrentProfileId } from './session-partition.js'; ··· 317 317 // (e.g. HUD with alwaysOnTop+focusable:false confuses getFocusedWindow()). 318 318 (app as any).on('did-become-active', () => { 319 319 getIzuiCoordinator().setAppFocused(true); 320 - publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: true }); 320 + publish(getSystemAddress(), 'app:focus-changed', { focused: true }); 321 321 // Show any alwaysOnTop overlay windows (e.g. HUD) that were hidden 322 322 // by the did-resign-active handler below. This hide/show pair 323 323 // avoids a pubsub round-trip through a renderer. ··· 332 332 const coordBefore = getIzuiCoordinator(); 333 333 DEBUG && console.log('[izui] did-resign-active fired. state:', coordBefore.getState(), 'isOverlay:', coordBefore.isOverlay(), 'exitCooldown:', coordBefore.isWithinOverlayExitCooldown(), 'entryCooldown:', coordBefore.isWithinOverlayEntryCooldown()); 334 334 coordBefore.setAppFocused(false); 335 - publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: false }); 335 + publish(getSystemAddress(), 'app:focus-changed', { focused: false }); 336 336 337 337 // Close overlay window (e.g. windows switcher) when app loses OS focus. 338 338 // The overlay is a transient full-screen picker that should dismiss when the user ··· 381 381 // Non-macOS fallback: use browser-window-focus/blur with focused-window check 382 382 app.on('browser-window-focus', () => { 383 383 getIzuiCoordinator().setAppFocused(true); 384 - publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: true }); 384 + publish(getSystemAddress(), 'app:focus-changed', { focused: true }); 385 385 }); 386 386 app.on('browser-window-blur', () => { 387 387 // Only mark unfocused if NO window has focus (user switched to another app). 388 388 // During window switches within Peek, one window blurs before another focuses. 389 389 if (!BrowserWindow.getFocusedWindow()) { 390 390 getIzuiCoordinator().setAppFocused(false); 391 - publish(getSystemAddress(), scopes.GLOBAL, 'app:focus-changed', { focused: false }); 391 + publish(getSystemAddress(), 'app:focus-changed', { focused: false }); 392 392 } 393 393 }); 394 394 } ··· 502 502 coordinator.popWindow(windowId); 503 503 } 504 504 505 - publish(windowData.source, scopes.GLOBAL, 'window:closed', { 505 + publish(windowData.source, 'window:closed', { 506 506 id: windowId, 507 507 source: windowData.source 508 508 }); ··· 684 684 const source = `peek://${extId}/`; 685 685 686 686 // Subscribe to execution requests from the cmd panel 687 - subscribe(source, scopes.GLOBAL, `cmd:execute:${cmd.name}`, (msg: unknown) => { 687 + subscribe(source, `cmd:execute:${cmd.name}`, (msg: unknown) => { 688 688 DEBUG && console.log(`[ext:declarative] Executing command: ${cmd.name}`); 689 689 executeDeclarativeAction(cmd, msg as Record<string, unknown>); 690 690 // Signal completion so the cmd panel proxy resolves immediately 691 691 // (publish-type actions are fire-and-forget — don't block the UI) 692 - publish(source, scopes.GLOBAL, `cmd:execute:${cmd.name}:result`, { success: true }); 692 + publish(source, `cmd:execute:${cmd.name}:result`, { success: true }); 693 693 }); 694 694 695 695 // Register the command with the cmd extension's registry 696 - publish(source, scopes.GLOBAL, 'cmd:register', { 696 + publish(source, 'cmd:register', { 697 697 name: cmd.name, 698 698 description: cmd.description || '', 699 699 source, ··· 722 722 DEBUG && console.log(`[ext:declarative] Shortcut triggered: ${shortcut.keys} -> ${shortcut.command}`); 723 723 if (cmd.action.type === 'execute') { 724 724 // For execute-type commands, publish to cmd:execute so lazy stubs can intercept 725 - publish(`peek://${extId}/`, scopes.GLOBAL, `cmd:execute:${cmd.name}`, {}); 725 + publish(`peek://${extId}/`, `cmd:execute:${cmd.name}`, {}); 726 726 } else { 727 727 executeDeclarativeAction(cmd, {}); 728 728 } ··· 753 753 754 754 // Use window:reopen-request which app/index.js already handles 755 755 // (creates a window via windowManager.createWindow) 756 - publish(getSystemAddress(), scopes.GLOBAL, 'window:reopen-request', { 756 + publish(getSystemAddress(), 'window:reopen-request', { 757 757 url, 758 758 options: { 759 759 ...options, ··· 769 769 const topic = action.topic; 770 770 const data = { ...(action.data || {}), ...ctx }; 771 771 DEBUG && console.log(`[ext:declarative] Publishing: ${topic}`); 772 - publish(getSystemAddress(), scopes.GLOBAL, topic, data); 772 + publish(getSystemAddress(), topic, data); 773 773 break; 774 774 } 775 775 ··· 851 851 const stubSource = `lazy-stub/${cmd.name}`; 852 852 const topic = `cmd:execute:${cmd.name}`; 853 853 854 - subscribe(stubSource, scopes.GLOBAL, topic, async (msg: unknown) => { 854 + subscribe(stubSource, topic, async (msg: unknown) => { 855 855 // Extension already loaded — forward directly and clean up 856 856 if (lazyExtensionLoaded.has(extId)) { 857 857 unsubscribe(stubSource, topic); 858 - publish(stubSource, scopes.GLOBAL, topic, msg); 858 + publish(stubSource, topic, msg); 859 859 return; 860 860 } 861 861 ··· 875 875 const bufferedMsg = lazyCommandPending.get(cmd.name); 876 876 lazyCommandPending.delete(cmd.name); 877 877 DEBUG && console.log(`[ext:lazy] Extension ${extId} loaded, re-publishing command: ${cmd.name}`); 878 - publish(stubSource, scopes.GLOBAL, topic, bufferedMsg); 878 + publish(stubSource, topic, bufferedMsg); 879 879 }); 880 880 881 881 // Register the command metadata with the cmd registry 882 882 const extSource = `peek://${extId}/`; 883 - publish(extSource, scopes.GLOBAL, 'cmd:register', { 883 + publish(extSource, 'cmd:register', { 884 884 name: cmd.name, 885 885 description: cmd.description || '', 886 886 source: extSource, ··· 966 966 for (const topic of editorEvents) { 967 967 const interceptorSource = `lazy-interceptor/${topic}`; 968 968 969 - subscribe(interceptorSource, scopes.GLOBAL, topic, async (msg: unknown) => { 969 + subscribe(interceptorSource, topic, async (msg: unknown) => { 970 970 // If editor is already loaded, nothing to do — the extension's own handler 971 971 // will fire from the same publish (pubsub iterates all subscribers). 972 972 if (lazyExtensionLoaded.has('editor')) return; ··· 988 988 const bufferedMsg = lazyInterceptorPending.get(topic); 989 989 lazyInterceptorPending.delete(topic); 990 990 DEBUG && console.log(`[ext:lazy-interceptor] Editor loaded, re-publishing ${topic}`); 991 - publish(interceptorSource, scopes.GLOBAL, topic, bufferedMsg); 991 + publish(interceptorSource, topic, bufferedMsg); 992 992 }); 993 993 } 994 994 ··· 1127 1127 const v2FeatureIds = new Set<string>(); 1128 1128 1129 1129 // Subscribe to ext:ready — extensions publish this after registering their handlers. 1130 - subscribe(getSystemAddress(), scopes.SYSTEM, 'ext:ready', (data: unknown) => { 1130 + subscribe(getSystemAddress(), 'ext:ready', (data: unknown) => { 1131 1131 const extId = (data as { id?: string })?.id; 1132 1132 const registeredTopics = (data as { registeredTopics?: string[] })?.registeredTopics; 1133 1133 if (extId) { ··· 1184 1184 } 1185 1185 1186 1186 // Phase 1: Early 1187 - publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'early' }); 1187 + publish('system', 'ext:startup:phase', { phase: 'early' }); 1188 1188 1189 1189 // Phase 2: Commands 1190 - publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'commands' }); 1190 + publish('system', 'ext:startup:phase', { phase: 'commands' }); 1191 1191 1192 1192 // Register declarative commands/shortcuts from manifests (after cmd is ready) 1193 1193 registerDeclarativeCommands(); ··· 1204 1204 DEBUG && console.log(`[ext:timing] hybrid total: ${Date.now() - extStart}ms`); 1205 1205 1206 1206 // Phase 3: UI 1207 - publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'ui' }); 1207 + publish('system', 'ext:startup:phase', { phase: 'ui' }); 1208 1208 1209 1209 // Phase 4: Complete 1210 - publish('system', scopes.GLOBAL, 'ext:startup:phase', { phase: 'complete' }); 1211 - publish('system', scopes.GLOBAL, 'ext:all-loaded', { count: 0 }); 1210 + publish('system', 'ext:startup:phase', { phase: 'complete' }); 1211 + publish('system', 'ext:all-loaded', { count: 0 }); 1212 1212 1213 1213 return 0; 1214 1214 } ··· 1592 1592 */ 1593 1593 export async function shutdown(): Promise<void> { 1594 1594 // Publish shutdown event 1595 - publish(getSystemAddress(), scopes.GLOBAL, 'app:shutdown', { 1595 + publish(getSystemAddress(), 'app:shutdown', { 1596 1596 timestamp: Date.now() 1597 1597 }); 1598 1598 ··· 1719 1719 function publishExternalUrl(url: string, sourceId: string): void { 1720 1720 const processUrl = () => { 1721 1721 // Note: Using trackingSource/trackingSourceId because preload.js overwrites msg.source 1722 - publish(getSystemAddress(), scopes.GLOBAL, 'external:open-url', { 1722 + publish(getSystemAddress(), 'external:open-url', { 1723 1723 url, 1724 1724 trackingSource: 'external', 1725 1725 trackingSourceId: sourceId, ··· 2012 2012 2013 2013 // Re-export commonly used functions 2014 2014 export { 2015 - scopes, 2016 2015 publish, 2017 2016 subscribe, 2018 2017 getSystemAddress,
+71 -70
backend/electron/pubsub.test.ts
··· 2 2 * Unit tests for PubSub module 3 3 */ 4 4 5 - import { describe, it, before, beforeEach } from 'node:test'; 5 + import { describe, it, before } from 'node:test'; 6 6 import * as assert from 'node:assert'; 7 7 8 8 let pubsub: typeof import('./pubsub.js'); ··· 15 15 describe('subscribe and publish', () => { 16 16 it('should deliver message to subscriber', () => { 17 17 let received: unknown = null; 18 - pubsub.subscribe('peek://test/a', pubsub.scopes.GLOBAL, 'test:basic', (msg) => { 18 + pubsub.subscribe('peek://test/a', 'test:basic', (msg) => { 19 19 received = msg; 20 20 }); 21 - pubsub.publish('peek://test/b', pubsub.scopes.GLOBAL, 'test:basic', { data: 42 }); 21 + pubsub.publish('peek://test/b', 'test:basic', { data: 42 }); 22 22 assert.deepStrictEqual(received, { data: 42 }); 23 23 pubsub.unsubscribe('peek://test/a', 'test:basic'); 24 24 }); 25 25 26 26 it('should deliver to multiple subscribers', () => { 27 27 let count = 0; 28 - pubsub.subscribe('peek://test/s1', pubsub.scopes.GLOBAL, 'test:multi', () => { count++; }); 29 - pubsub.subscribe('peek://test/s2', pubsub.scopes.GLOBAL, 'test:multi', () => { count++; }); 30 - pubsub.publish('peek://test/pub', pubsub.scopes.GLOBAL, 'test:multi', {}); 28 + pubsub.subscribe('peek://test/s1', 'test:multi', () => { count++; }); 29 + pubsub.subscribe('peek://test/s2', 'test:multi', () => { count++; }); 30 + pubsub.publish('peek://test/pub', 'test:multi', {}); 31 31 assert.strictEqual(count, 2); 32 32 pubsub.unsubscribe('peek://test/s1', 'test:multi'); 33 33 pubsub.unsubscribe('peek://test/s2', 'test:multi'); ··· 35 35 36 36 it('should not throw when publishing with no subscribers', () => { 37 37 assert.doesNotThrow(() => { 38 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:nosub', {}); 38 + pubsub.publish('peek://test/x', 'test:nosub', {}); 39 39 }); 40 40 }); 41 41 42 42 it('should replace callback when same source subscribes again', () => { 43 43 let first = false; 44 44 let second = false; 45 - pubsub.subscribe('peek://test/dup', pubsub.scopes.GLOBAL, 'test:replace', () => { first = true; }); 46 - pubsub.subscribe('peek://test/dup', pubsub.scopes.GLOBAL, 'test:replace', () => { second = true; }); 47 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:replace', {}); 45 + pubsub.subscribe('peek://test/dup', 'test:replace', () => { first = true; }); 46 + pubsub.subscribe('peek://test/dup', 'test:replace', () => { second = true; }); 47 + pubsub.publish('peek://test/x', 'test:replace', {}); 48 48 assert.strictEqual(first, false); 49 49 assert.strictEqual(second, true); 50 50 pubsub.unsubscribe('peek://test/dup', 'test:replace'); 51 51 }); 52 52 }); 53 53 54 - describe('scope filtering', () => { 55 - it('GLOBAL scope delivers to all subscribers', () => { 56 - let received = false; 57 - pubsub.subscribe('peek://other/sub', pubsub.scopes.GLOBAL, 'test:scope:global', () => { received = true; }); 58 - pubsub.publish('peek://test/pub', pubsub.scopes.GLOBAL, 'test:scope:global', {}); 59 - assert.strictEqual(received, true); 60 - pubsub.unsubscribe('peek://other/sub', 'test:scope:global'); 61 - }); 62 - 63 - it('SELF scope delivers only to same pseudo-host', () => { 64 - let sameHost = false; 65 - let diffHost = false; 66 - pubsub.subscribe('peek://app/sub1', pubsub.scopes.GLOBAL, 'test:scope:self', () => { sameHost = true; }); 67 - pubsub.subscribe('peek://other/sub2', pubsub.scopes.GLOBAL, 'test:scope:self', () => { diffHost = true; }); 68 - pubsub.publish('peek://app/pub', pubsub.scopes.SELF, 'test:scope:self', {}); 69 - assert.strictEqual(sameHost, true); 70 - assert.strictEqual(diffHost, false); 71 - pubsub.unsubscribe('peek://app/sub1', 'test:scope:self'); 72 - pubsub.unsubscribe('peek://other/sub2', 'test:scope:self'); 73 - }); 74 - 75 - it('SYSTEM address receives all scopes', () => { 76 - let received = false; 77 - const sysAddr = pubsub.getSystemAddress(); 78 - pubsub.subscribe(sysAddr, pubsub.scopes.GLOBAL, 'test:scope:sys', () => { received = true; }); 79 - pubsub.publish('peek://test/pub', pubsub.scopes.SELF, 'test:scope:sys', {}); 80 - assert.strictEqual(received, true); 81 - pubsub.unsubscribe(sysAddr, 'test:scope:sys'); 82 - }); 83 - }); 84 - 85 54 describe('unsubscribe', () => { 86 55 it('should remove subscriber and stop delivery', () => { 87 56 let count = 0; 88 - pubsub.subscribe('peek://test/unsub', pubsub.scopes.GLOBAL, 'test:unsub', () => { count++; }); 89 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:unsub', {}); 57 + pubsub.subscribe('peek://test/unsub', 'test:unsub', () => { count++; }); 58 + pubsub.publish('peek://test/x', 'test:unsub', {}); 90 59 assert.strictEqual(count, 1); 91 60 pubsub.unsubscribe('peek://test/unsub', 'test:unsub'); 92 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:unsub', {}); 61 + pubsub.publish('peek://test/x', 'test:unsub', {}); 93 62 assert.strictEqual(count, 1); // No additional delivery 94 63 }); 95 64 ··· 101 70 describe('unsubscribeAll', () => { 102 71 it('should remove all subscriptions for a source', () => { 103 72 let a = 0, b = 0; 104 - pubsub.subscribe('peek://test/all', pubsub.scopes.GLOBAL, 'test:all:a', () => { a++; }); 105 - pubsub.subscribe('peek://test/all', pubsub.scopes.GLOBAL, 'test:all:b', () => { b++; }); 73 + pubsub.subscribe('peek://test/all', 'test:all:a', () => { a++; }); 74 + pubsub.subscribe('peek://test/all', 'test:all:b', () => { b++; }); 106 75 pubsub.unsubscribeAll('peek://test/all'); 107 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:all:a', {}); 108 - pubsub.publish('peek://test/x', pubsub.scopes.GLOBAL, 'test:all:b', {}); 76 + pubsub.publish('peek://test/x', 'test:all:a', {}); 77 + pubsub.publish('peek://test/x', 'test:all:b', {}); 109 78 assert.strictEqual(a, 0); 110 79 assert.strictEqual(b, 0); 111 80 }); ··· 113 82 114 83 describe('hasSubscriber', () => { 115 84 it('should return true when subscriber exists', () => { 116 - pubsub.subscribe('peek://test/has', pubsub.scopes.GLOBAL, 'test:has', () => {}); 85 + pubsub.subscribe('peek://test/has', 'test:has', () => {}); 117 86 assert.strictEqual(pubsub.hasSubscriber('test:has'), true); 118 87 pubsub.unsubscribe('peek://test/has', 'test:has'); 119 88 }); ··· 123 92 }); 124 93 125 94 it('should return false after unsubscribe', () => { 126 - pubsub.subscribe('peek://test/gone', pubsub.scopes.GLOBAL, 'test:gone', () => {}); 95 + pubsub.subscribe('peek://test/gone', 'test:gone', () => {}); 127 96 pubsub.unsubscribe('peek://test/gone', 'test:gone'); 128 97 assert.strictEqual(pubsub.hasSubscriber('test:gone'), false); 129 98 }); 130 99 131 100 it('should filter by source when provided', () => { 132 - pubsub.subscribe('peek://test/src1', pubsub.scopes.GLOBAL, 'test:has:src', () => {}); 101 + pubsub.subscribe('peek://test/src1', 'test:has:src', () => {}); 133 102 assert.strictEqual(pubsub.hasSubscriber('test:has:src', 'peek://test/src1'), true); 134 103 assert.strictEqual(pubsub.hasSubscriber('test:has:src', 'peek://test/other'), false); 135 104 pubsub.unsubscribe('peek://test/src1', 'test:has:src'); 136 105 }); 137 106 138 107 it('should exclude subscribers by prefix', () => { 139 - pubsub.subscribe('lazy-stub/foo', pubsub.scopes.GLOBAL, 'test:has:excl', () => {}); 108 + pubsub.subscribe('lazy-stub/foo', 'test:has:excl', () => {}); 140 109 // Only stub subscriber — should return false when excluding stubs 141 110 assert.strictEqual(pubsub.hasSubscriber('test:has:excl', undefined, 'lazy-stub/'), false); 142 111 // Add a real subscriber 143 - pubsub.subscribe('peek://real/', pubsub.scopes.GLOBAL, 'test:has:excl', () => {}); 112 + pubsub.subscribe('peek://real/', 'test:has:excl', () => {}); 144 113 assert.strictEqual(pubsub.hasSubscriber('test:has:excl', undefined, 'lazy-stub/'), true); 145 114 pubsub.unsubscribe('lazy-stub/foo', 'test:has:excl'); 146 115 pubsub.unsubscribe('peek://real/', 'test:has:excl'); ··· 148 117 }); 149 118 150 119 describe('setPubsubBroadcaster', () => { 151 - it('GLOBAL publishes invoke the registered broadcaster with topic/msg/source', () => { 120 + it('publishes invoke the registered broadcaster with topic/msg/source', () => { 152 121 const calls: Array<{ topic: string; msg: unknown; source: string }> = []; 153 122 pubsub.setPubsubBroadcaster((topic, msg, source) => { 154 123 calls.push({ topic, msg, source }); 155 124 }); 156 - pubsub.publish('peek://tile-foo/entry', pubsub.scopes.GLOBAL, 'test:broadcaster:fanout', { n: 1 }); 125 + pubsub.publish('peek://tile-foo/entry', 'test:broadcaster:fanout', { n: 1 }); 157 126 assert.strictEqual(calls.length, 1); 158 127 assert.strictEqual(calls[0].topic, 'test:broadcaster:fanout'); 159 128 assert.deepStrictEqual(calls[0].msg, { n: 1 }); ··· 162 131 pubsub.setPubsubBroadcaster(() => {}); 163 132 }); 164 133 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 134 it('broadcaster receives a v2-tile publish of cmd:execute:*:result so bgWindow-subscriber dispatchers can resolve', () => { 174 135 // 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. 136 + // result topic. The main.ts broadcaster callback is responsible for 137 + // forwarding this to bgWindow (core), which is where the dispatcher 138 + // that set `resultTopic` lives. If the broadcaster is not invoked 139 + // for publishes, bgWindow never resolves the pending result promise 140 + // and tag/untag/widget-update smoke tests time out at 10s. 181 141 const received: Array<{ topic: string; source: string }> = []; 182 142 pubsub.setPubsubBroadcaster((topic, _msg, source) => { 183 143 received.push({ topic, source }); 184 144 }); 185 145 pubsub.publish( 186 146 'peek://tags/background', 187 - pubsub.scopes.GLOBAL, 188 147 'cmd:execute:tag:result:abc-123', 189 148 { ok: true }, 190 149 ); ··· 192 151 assert.strictEqual(received[0].topic, 'cmd:execute:tag:result:abc-123'); 193 152 assert.strictEqual(received[0].source, 'peek://tags/background'); 194 153 pubsub.setPubsubBroadcaster(() => {}); 154 + }); 155 + }); 156 + 157 + // ─── Phase 6: scope is gone ────────────────────────────────────── 158 + // 159 + // Regression guard for Phase 6 (see docs/pubsub-state-machine.md §Phased 160 + // implementation item 6). Scope (SYSTEM / SELF / GLOBAL) was removed 161 + // entirely — no runtime filter, no exported constants, no 4-argument 162 + // publish/subscribe shape. Privilege moved onto the topic name + 163 + // capability grant (topic allowlist in tile-ipc.ts). 164 + describe('Phase 6: scope is gone', () => { 165 + it('pubsub module does not export `scopes` or `Scope`', () => { 166 + const exports = Object.keys(pubsub as unknown as Record<string, unknown>); 167 + assert.ok(!exports.includes('scopes'), `pubsub exports scopes: ${exports.join(',')}`); 168 + // Type-only exports don't appear on the runtime module object, but 169 + // this assertion also guards against an accidental runtime re-export. 170 + assert.strictEqual( 171 + (pubsub as unknown as Record<string, unknown>).scopes, 172 + undefined, 173 + ); 174 + }); 175 + 176 + it('publish accepts (source, topic, msg) — 3 arguments, no scope slot', () => { 177 + // Function.length returns the arity (count of required args before 178 + // any default / rest). pubsub.publish is declared with exactly 179 + // three required parameters after Phase 6. 180 + assert.strictEqual(pubsub.publish.length, 3); 181 + }); 182 + 183 + it('subscribe accepts (source, topic, cb) — 3 arguments, no scope slot', () => { 184 + assert.strictEqual(pubsub.subscribe.length, 3); 185 + }); 186 + 187 + it('every subscriber receives every matching publish regardless of source', () => { 188 + // Pre-Phase-6 SELF scope would have filtered out delivery between 189 + // different pseudo-hosts. With scope deleted, cross-host delivery 190 + // is the default. 191 + let crossHost = 0; 192 + pubsub.subscribe('peek://app/x', 'phase6:xhost', () => { crossHost++; }); 193 + pubsub.publish('peek://other/y', 'phase6:xhost', {}); 194 + assert.strictEqual(crossHost, 1); 195 + pubsub.unsubscribe('peek://app/x', 'phase6:xhost'); 195 196 }); 196 197 }); 197 198 });
+22 -57
backend/electron/pubsub.ts
··· 3 3 * 4 4 * Handles: 5 5 * - Topic-based publish/subscribe 6 - * - Scope-based message filtering (SYSTEM, SELF, GLOBAL) 7 6 * - Renderer window broadcasting (via callback) 8 7 * 9 - * NOTE: The scope/topic logic is mirrored in app/lib/pubsub.js (the shared 10 - * renderer-layer PubSub engine). This file stays separate because the main 11 - * process acts as an IPC hub and cannot import ES modules from app/lib/. 12 - * If you change scope semantics here, update app/lib/pubsub.js too. 8 + * Scope (SYSTEM / SELF / GLOBAL) was removed in Phase 6 of the pubsub 9 + * state-machine plan (see docs/pubsub-state-machine.md). Every subscriber 10 + * now receives every publish whose topic it subscribed to; privilege is 11 + * enforced by capability grants (topic allowlists) and by topic naming, 12 + * not by a runtime scope filter. 13 13 */ 14 - 15 - // Message scopes 16 - export const scopes = { 17 - SYSTEM: 1, 18 - SELF: 2, 19 - GLOBAL: 3 20 - } as const; 21 - 22 - export type Scope = typeof scopes[keyof typeof scopes]; 23 14 24 15 // System address for privileged subscribers 25 16 const SYSTEM_ADDRESS = 'peek://system/'; ··· 62 53 }; 63 54 64 55 /** 65 - * Extract pseudo-host from a peek:// URL 66 - * e.g., 'peek://app/foo.html' -> 'app' 67 - */ 68 - function getPseudoHost(str: string): string { 69 - return str.split('/')[2] || ''; 70 - } 71 - 72 - /** 73 - * Check if a subscriber should receive a message based on scope 74 - */ 75 - function scopeCheck(pubSource: string, subSource: string, scope: Scope): boolean { 76 - // System address receives everything 77 - if (subSource === SYSTEM_ADDRESS) { 78 - return true; 79 - } 80 - // GLOBAL scope sends to everyone 81 - if (scope === scopes.GLOBAL) { 82 - return true; 83 - } 84 - // SELF scope only sends to same pseudo-host 85 - if (getPseudoHost(subSource) === getPseudoHost(pubSource)) { 86 - return true; 87 - } 88 - return false; 89 - } 90 - 91 - /** 92 56 * Set the callback for broadcasting to renderer windows 93 57 * This is called from the main process to inject the window broadcasting logic 94 58 */ ··· 106 70 * delivery until the promise resolves; a `'skip'` result aborts the 107 71 * publish entirely. See `registerPrePublishHook` for the mechanism. 108 72 */ 109 - export function publish(source: string, scope: Scope, topic: string, msg: unknown): void { 73 + export function publish(source: string, topic: string, msg: unknown): void { 110 74 // Track session stats — counted once per publish call regardless of 111 75 // whether a hook defers/skips delivery. Stats reflect publish intent. 112 76 sessionStats.messagesPublished++; ··· 114 78 115 79 const hook = findPrePublishHook(topic); 116 80 if (!hook) { 117 - deliver(source, scope, topic, msg); 81 + deliver(source, topic, msg); 118 82 return; 119 83 } 120 84 ··· 123 87 result = hook(topic, msg); 124 88 } catch (err) { 125 89 console.error(`[pubsub] pre-publish hook threw for ${topic}:`, err); 126 - deliver(source, scope, topic, msg); 90 + deliver(source, topic, msg); 127 91 return; 128 92 } 129 93 ··· 131 95 (result as Promise<PrePublishHookResult>).then( 132 96 (val) => { 133 97 if (val === 'skip') return; 134 - deliver(source, scope, topic, msg); 98 + deliver(source, topic, msg); 135 99 }, 136 100 (err) => { 137 101 // Hook failed mid-flight — best-effort deliver anyway so a 138 102 // broken loader can't hang every matching publish forever. 139 103 console.error(`[pubsub] pre-publish hook rejected for ${topic}:`, err); 140 - deliver(source, scope, topic, msg); 104 + deliver(source, topic, msg); 141 105 }, 142 106 ); 143 107 return; 144 108 } 145 109 146 110 if (result === 'skip') return; 147 - deliver(source, scope, topic, msg); 111 + deliver(source, topic, msg); 148 112 } 149 113 150 114 function findPrePublishHook(topic: string): PrePublishHook | null { ··· 158 122 return null; 159 123 } 160 124 161 - function deliver(source: string, scope: Scope, topic: string, msg: unknown): void { 162 - // Route to traditional subscribers (via IPC callbacks) 125 + function deliver(source: string, topic: string, msg: unknown): void { 126 + // Route to traditional subscribers (via IPC callbacks) — every subscriber 127 + // on this topic receives the message. Scope filtering was removed in 128 + // Phase 6; privilege is enforced by capability grants and topic naming. 163 129 if (topics.has(topic)) { 164 130 const t = topics.get(topic)!; 165 - for (const [subSource, cb] of t) { 166 - if (scopeCheck(source, subSource, scope)) { 167 - sessionStats.messagesDelivered++; 168 - cb(msg); 169 - } 131 + for (const [, cb] of t) { 132 + sessionStats.messagesDelivered++; 133 + cb(msg); 170 134 } 171 135 } 172 136 173 - // Route to renderer windows (GLOBAL scope only) 174 - if (scope === scopes.GLOBAL && pubsubBroadcaster) { 137 + // Route to renderer windows. In Phase 5 this was gated on GLOBAL scope; 138 + // with scope gone, every publish routes to the broadcaster so v2 tile 139 + // windows and bgWindow subscribers see the event. 140 + if (pubsubBroadcaster) { 175 141 pubsubBroadcaster(topic, msg, source); 176 142 } 177 143 } ··· 205 171 */ 206 172 export function subscribe( 207 173 source: string, 208 - scope: Scope, 209 174 topic: string, 210 175 cb: (msg: unknown) => void 211 176 ): void {
+3 -3
backend/electron/session.ts
··· 19 19 import type { ClosedWindowEntry } from './main.js'; 20 20 import { getRegisteredExtensionIds } from './protocol.js'; 21 21 22 - import { publish, scopes as PubSubScopes, getSystemAddress } from './pubsub.js'; 22 + import { publish, getSystemAddress } from './pubsub.js'; 23 23 import { DEBUG, isTestProfile } from './config.js'; 24 24 25 25 export interface WindowContext { ··· 747 747 if (result.failed > 0 && result.total > 2 && result.failed > result.total / 2) { 748 748 console.warn(`[session] Partial restore: ${result.failed}/${result.total} windows failed`); 749 749 try { 750 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'session:restore-partial', { 750 + publish(getSystemAddress(), 'session:restore-partial', { 751 751 restored: result.restored, 752 752 failed: result.failed, 753 753 total: result.total, ··· 759 759 760 760 // Publish session:restored event 761 761 try { 762 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'session:restored', { 762 + publish(getSystemAddress(), 'session:restored', { 763 763 restored: result.restored, 764 764 failed: result.failed, 765 765 total: result.total,
+3 -3
backend/electron/tag-events.ts
··· 15 15 */ 16 16 17 17 import { getDb, tagItem, untagItem } from './datastore.js'; 18 - import { publish, scopes as PubSubScopes } from './pubsub.js'; 18 + import { publish } from './pubsub.js'; 19 19 20 20 /** 21 21 * Tag an item and publish `tag:item-added` on the GLOBAL scope so UIs ··· 34 34 const db = getDb(); 35 35 const tag = db.prepare('SELECT name FROM tags WHERE id = ?').get(tagId) as { name: string } | undefined; 36 36 const item = db.prepare('SELECT type FROM items WHERE id = ?').get(itemId) as { type: string } | undefined; 37 - publish('system', PubSubScopes.GLOBAL, 'tag:item-added', { 37 + publish('system', 'tag:item-added', { 38 38 tagId, 39 39 tagName: tag?.name, 40 40 itemId, ··· 54 54 const tag = db.prepare('SELECT name FROM tags WHERE id = ?').get(tagId) as { name: string } | undefined; 55 55 const removed = untagItem(itemId, tagId); 56 56 if (removed) { 57 - publish('system', PubSubScopes.GLOBAL, 'tag:item-removed', { 57 + publish('system', 'tag:item-removed', { 58 58 tagId, 59 59 tagName: tag?.name, 60 60 itemId,
+3 -14
backend/electron/tile-api.d.ts
··· 17 17 * // or place in tsconfig.json "typeRoots" / "types" array 18 18 */ 19 19 20 - // ─── Scope constants ───────────────────────────────────────────────── 21 - 22 - interface TileScopes { 23 - readonly SELF: 2; 24 - readonly GLOBAL: 3; 25 - } 26 - 27 20 // ─── PubSub ────────────────────────────────────────────────────────── 28 21 29 22 interface TilePubSub { 30 23 /** 31 24 * Publish a message to a topic. 32 - * Requires `pubsub` capability with an appropriate scope grant. 25 + * Requires `pubsub` capability. 33 26 * @param topic Event topic string 34 27 * @param data Payload (any serialisable value) 35 - * @param scope Optional scope: api.scopes.SELF (2) or api.scopes.GLOBAL (3). Default: SELF. 36 28 */ 37 - publish(topic: string, data: unknown, scope?: number): void; 29 + publish(topic: string, data: unknown): void; 38 30 39 31 /** 40 32 * Subscribe to a topic. 41 33 * Requires `pubsub` capability. 42 34 * @returns Unsubscribe function 43 35 */ 44 - subscribe(topic: string, callback: (data: unknown) => void, scope?: number): () => void; 36 + subscribe(topic: string, callback: (data: unknown) => void): () => void; 45 37 } 46 38 47 39 // ─── Commands ──────────────────────────────────────────────────────── ··· 568 560 569 561 /** Register a shutdown handler. */ 570 562 onShutdown(callback: () => void): void; 571 - 572 - /** Scope constants for pubsub. */ 573 - readonly scopes: TileScopes; 574 563 575 564 /** Whether the app is running in debug mode. */ 576 565 readonly debug: boolean;
+8 -8
backend/electron/tile-command-registration.test.ts
··· 17 17 import * as assert from 'node:assert'; 18 18 19 19 import { 20 - publish, subscribe, unsubscribe, scopes, 20 + publish, subscribe, unsubscribe, 21 21 __clearPrePublishHooksForTest, 22 22 } from './pubsub.js'; 23 23 import { ··· 108 108 109 109 it('publishes cmd:register-batch announcing every manifest command', () => { 110 110 const registered: unknown[] = []; 111 - subscribe('test-observer', scopes.GLOBAL, 'cmd:register-batch', (msg: unknown) => { 111 + subscribe('test-observer', 'cmd:register-batch', (msg: unknown) => { 112 112 registered.push(msg); 113 113 }); 114 114 ··· 130 130 131 131 it('preserves params metadata in cmd:register-batch', () => { 132 132 const received: unknown[] = []; 133 - subscribe('test-observer-2', scopes.GLOBAL, 'cmd:register-batch', (msg: unknown) => { 133 + subscribe('test-observer-2', 'cmd:register-batch', (msg: unknown) => { 134 134 received.push(msg); 135 135 }); 136 136 ··· 161 161 162 162 const realHandlerReceived: unknown[] = []; 163 163 launcherState.onLaunch = () => { 164 - subscribe('peek://cmd-test-4/bg', scopes.GLOBAL, 'cmd:execute:doit', (msg: unknown) => { 164 + subscribe('peek://cmd-test-4/bg', 'cmd:execute:doit', (msg: unknown) => { 165 165 realHandlerReceived.push(msg); 166 166 }); 167 167 signalReady(launcherState, 'cmd-test-4', 'background'); 168 168 }; 169 169 170 - publish('test-caller', scopes.GLOBAL, 'cmd:execute:doit', { payload: 42 }); 170 + publish('test-caller', 'cmd:execute:doit', { payload: 42 }); 171 171 172 172 // Dispatch hook is async — yield enough microtasks for load, 173 173 // ready-signal, hook continuation, and delivery. ··· 182 182 183 183 it('window-action commands are announced in the batch and skip tile load on dispatch', async () => { 184 184 const received: unknown[] = []; 185 - subscribe('test-observer-3', scopes.GLOBAL, 'cmd:register-batch', (msg: unknown) => { 185 + subscribe('test-observer-3', 'cmd:register-batch', (msg: unknown) => { 186 186 received.push(msg); 187 187 }); 188 188 189 189 const windowOpenReceived: unknown[] = []; 190 - subscribe('test-window-observer', scopes.GLOBAL, 'window:reopen-request', (msg: unknown) => { 190 + subscribe('test-window-observer', 'window:reopen-request', (msg: unknown) => { 191 191 windowOpenReceived.push(msg); 192 192 }); 193 193 ··· 207 207 208 208 // Firing the command should publish window:reopen-request and NOT 209 209 // load the tile (declarative window actions bypass tile boot). 210 - publish('test-caller', scopes.GLOBAL, 'cmd:execute:open-window', {}); 210 + publish('test-caller', 'cmd:execute:open-window', {}); 211 211 for (let i = 0; i < 3; i++) await flushMicrotasks(); 212 212 213 213 assert.strictEqual(launcherState.launchCalls.length, 0, 'declarative window action must not load the tile');
+2 -2
backend/electron/tile-ipc-sender-check.ts
··· 39 39 */ 40 40 41 41 import { getGrantForToken, getTokenOwner, setTokenOwner } from './tile-tokens.js'; 42 - import { publish, scopes, getSystemAddress } from './pubsub.js'; 42 + import { publish, getSystemAddress } from './pubsub.js'; 43 43 import type { CapabilityGrant } from './tile-manifest.js'; 44 44 45 45 /** ··· 51 51 export function emitTileDrift(reason: string, ctx: Record<string, unknown>): void { 52 52 const payload = { reason, ...ctx, ts: Date.now() }; 53 53 try { 54 - publish(getSystemAddress(), scopes.GLOBAL, 'tile:drift', payload); 54 + publish(getSystemAddress(), 'tile:drift', payload); 55 55 } catch (err) { 56 56 // Publishing drift must never throw out of the handler — swallow 57 57 // and log so the original rejection path stays clean.
+2 -2
backend/electron/tile-ipc.test.ts
··· 28 28 getTokenOwner, 29 29 } from './tile-tokens.js'; 30 30 import { resolveCapabilities } from './tile-manifest.js'; 31 - import { subscribe, unsubscribe, scopes } from './pubsub.js'; 31 + import { subscribe, unsubscribe } from './pubsub.js'; 32 32 import { verifyTokenSender } from './tile-ipc-sender-check.js'; 33 33 34 34 // ─── Helpers ───────────────────────────────────────────────────────── ··· 56 56 function captureDriftEvents(): { events: Array<Record<string, unknown>>; stop: () => void } { 57 57 const events: Array<Record<string, unknown>> = []; 58 58 const source = `test-drift-listener-${Math.random().toString(36).slice(2, 8)}`; 59 - subscribe(source, scopes.GLOBAL, 'tile:drift', (msg) => { 59 + subscribe(source, 'tile:drift', (msg) => { 60 60 events.push(msg as Record<string, unknown>); 61 61 }); 62 62 return {
+23 -57
backend/electron/tile-ipc.ts
··· 31 31 } from './tile-launcher.js'; 32 32 import { verifyTokenSender } from './tile-ipc-sender-check.js'; 33 33 import { parseManifestFile } from './tile-manifest.js'; 34 - import { publish, subscribe, unsubscribe, scopes, type Scope } from './pubsub.js'; 34 + import { publish, subscribe, unsubscribe } from './pubsub.js'; 35 35 import { DEBUG, TILE_STRICT, isHeadless } from './config.js'; 36 36 import { publishCapabilityViolation } from './tile-violations.js'; 37 37 import { getFeatureRegistry } from './feature-startup.js'; ··· 232 232 } 233 233 234 234 /** 235 - * Check if a pubsub scope level is granted. Trusted-builtin core 236 - * renderers bypass the scope allowlist. 237 - */ 238 - function hasPubSubScope(grant: CapabilityGrant, scope: 'self' | 'global' | 'system'): boolean { 239 - if (grant.trustedBuiltin) return true; 240 - if (!grant.capabilities.pubsub) return false; 241 - return grant.capabilities.pubsub.scopes.includes(scope); 242 - } 243 - 244 - /** 245 235 * Check if a network domain is allowed 246 236 * Supports wildcard matching (e.g., '*.example.com' matches 'api.example.com'). 247 237 * Trusted-builtin core renderers bypass the domain allowlist. ··· 297 287 return false; 298 288 } 299 289 300 - /** 301 - * Map pubsub scope number to name for capability checking 302 - */ 303 - function scopeNumberToName(scopeNum: number): 'self' | 'global' | 'system' { 304 - switch (scopeNum) { 305 - case scopes.SYSTEM: return 'system'; 306 - case scopes.GLOBAL: return 'global'; 307 - default: return 'self'; 308 - } 309 - } 310 - 311 290 // ─── Semver Comparison Helper ─────────────────────────────────────── 312 291 313 292 /** ··· 484 463 ipcMain.on('tile:pubsub:publish', (event, args: { 485 464 token: string; 486 465 source: string; 487 - scope: number; 488 466 topic: string; 489 467 data: unknown; 490 468 }) => { ··· 494 472 return; 495 473 } 496 474 497 - // Check pubsub scope capability 498 - const scopeName = scopeNumberToName(args.scope); 499 - if (!hasPubSubScope(grant, scopeName)) { 500 - const reason = `scope ${scopeName} not granted for ${grant.tileId}`; 501 - handleViolation(grant, 'pubsub', 'pubsub:publish', reason, args.token); 502 - return; 503 - } 504 - 505 475 // Topic enforcement: if topics allowlist is set, check it. 506 476 // Trusted-builtin core renderers bypass the topic allowlist. 507 477 // ··· 549 519 } 550 520 } 551 521 552 - publish(args.source, args.scope as Scope, args.topic, args.data); 522 + publish(args.source, args.topic, args.data); 553 523 }); 554 524 555 525 ipcMain.on('tile:pubsub:subscribe', (event, args: { ··· 560 530 const grant = verifyTokenSender(event, args.token, 'tile:pubsub:subscribe'); 561 531 if (!grant) return; 562 532 563 - // Subscribe at GLOBAL scope (main process will filter via scopeCheck) 564 - subscribe(args.source, scopes.GLOBAL, args.topic, (msg) => { 533 + subscribe(args.source, args.topic, (msg) => { 565 534 // Message delivery handled by extension broadcaster in main.ts 566 535 }); 567 536 }); ··· 604 573 // full publish arrives second and overwrites the minimal. 605 574 publish( 606 575 `peek://${args.tileId}/background`, 607 - scopes.GLOBAL, 608 576 'cmd:register-batch', 609 577 { 610 578 commands: [{ ··· 807 775 // the legacy context-set handler in ipc.ts). 808 776 publish( 809 777 source, 810 - scopes.GLOBAL, 811 778 'context:changed', 812 779 { 813 780 key: args.key, ··· 1260 1227 } 1261 1228 } 1262 1229 1263 - publish('feature-registry', scopes.GLOBAL, 'feature:enabled', { id: args.id }); 1230 + publish('feature-registry', 'feature:enabled', { id: args.id }); 1264 1231 } 1265 1232 return { success: result }; 1266 1233 }); ··· 1304 1271 } 1305 1272 } 1306 1273 1307 - publish('feature-registry', scopes.GLOBAL, 'feature:disabled', { id: args.id }); 1274 + publish('feature-registry', 'feature:disabled', { id: args.id }); 1308 1275 } 1309 1276 return { success: result }; 1310 1277 }); ··· 1368 1335 entry.grantedCapabilities as unknown as Record<string, unknown> 1369 1336 ); 1370 1337 1371 - publish('feature-registry', scopes.GLOBAL, 'feature:removed', { id: args.id }); 1338 + publish('feature-registry', 'feature:removed', { id: args.id }); 1372 1339 } 1373 1340 return { success: result }; 1374 1341 }); ··· 1472 1439 result.entry!.grantedCapabilities as unknown as Record<string, unknown> 1473 1440 ); 1474 1441 1475 - publish('feature-registry', scopes.GLOBAL, 'feature:installed', { 1442 + publish('feature-registry', 'feature:installed', { 1476 1443 id: result.entry!.id, 1477 1444 name: result.entry!.name, 1478 1445 source: metadata.source, ··· 1731 1698 installResult.entry?.grantedCapabilities as unknown as Record<string, unknown>, 1732 1699 ); 1733 1700 1734 - publish('feature-registry', scopes.GLOBAL, 'feature:updated', { 1701 + publish('feature-registry', 'feature:updated', { 1735 1702 id: args.id, 1736 1703 name: entry.name, 1737 1704 previousVersion: currentVersion, ··· 2065 2032 2066 2033 writeFeatureHistory(featureId, 'install', '0.1.0', 'dev'); 2067 2034 2068 - publish('feature-registry', scopes.GLOBAL, 'feature:installed', { 2035 + publish('feature-registry', 'feature:installed', { 2069 2036 id: featureId, 2070 2037 name: featureName, 2071 2038 source: { type: 'dev' }, ··· 3489 3456 } 3490 3457 publish( 3491 3458 `peek://${grant.tileId}/background`, 3492 - scopes.GLOBAL, 3493 3459 'cmd:unregister', 3494 3460 { name: args.name }, 3495 3461 ); ··· 4174 4140 // Notify all windows that sync pulled new data so they can refresh. 4175 4141 // Mirrors the behaviour of the legacy `sync-full` handler. 4176 4142 if (result.pulled > 0) { 4177 - publish('system', scopes.GLOBAL, 'sync:pull-completed', { 4143 + publish('system', 'sync:pull-completed', { 4178 4144 pulled: result.pulled, 4179 4145 pushed: result.pushed, 4180 4146 conflicts: result.conflicts, ··· 4345 4311 try { 4346 4312 // dsAddItem takes ItemType; trust the caller to pass a valid one. 4347 4313 const result = dsAddItem(args.type as Parameters<typeof dsAddItem>[0], (args.options as Parameters<typeof dsAddItem>[1]) || {}); 4348 - publish('system', scopes.GLOBAL, 'item:created', { 4314 + publish('system', 'item:created', { 4349 4315 itemId: result.id, 4350 4316 itemType: args.type, 4351 4317 content: (args.options as Record<string, unknown> | undefined)?.content, ··· 4386 4352 if (result) { 4387 4353 const db = getDb(); 4388 4354 const item = db.prepare('SELECT type FROM items WHERE id = ?').get(args.id) as { type: string } | undefined; 4389 - publish('system', scopes.GLOBAL, 'item:updated', { 4355 + publish('system', 'item:updated', { 4390 4356 itemId: args.id, 4391 4357 itemType: item?.type, 4392 4358 }); ··· 4410 4376 const item = db.prepare('SELECT type FROM items WHERE id = ?').get(args.id) as { type: string } | undefined; 4411 4377 const result = dsDeleteItem(args.id); 4412 4378 if (result) { 4413 - publish('system', scopes.GLOBAL, 'item:deleted', { 4379 + publish('system', 'item:deleted', { 4414 4380 itemId: args.id, 4415 4381 itemType: item?.type, 4416 4382 }); ··· 4490 4456 const db = getDb(); 4491 4457 const tag = db.prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 4492 4458 const item = db.prepare('SELECT type FROM items WHERE id = ?').get(args.itemId) as { type: string } | undefined; 4493 - publish('system', scopes.GLOBAL, 'tag:item-added', { 4459 + publish('system', 'tag:item-added', { 4494 4460 tagId: args.tagId, 4495 4461 tagName: tag?.name, 4496 4462 itemId: args.itemId, ··· 4516 4482 const tag = getDb().prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 4517 4483 const result = dsUntagItem(args.itemId, args.tagId); 4518 4484 if (result) { 4519 - publish('system', scopes.GLOBAL, 'tag:item-removed', { 4485 + publish('system', 'tag:item-removed', { 4520 4486 tagId: args.tagId, 4521 4487 tagName: tag?.name, 4522 4488 itemId: args.itemId, ··· 4941 4907 if (opts.favicon) { 4942 4908 getDb().prepare('UPDATE items SET favicon = ? WHERE id = ?').run(opts.favicon as string, result.id); 4943 4909 } 4944 - publish('system', scopes.GLOBAL, 'item:created', { 4910 + publish('system', 'item:created', { 4945 4911 itemId: result.id, itemType: 'url', content: normalizedUri, 4946 4912 }); 4947 4913 return { success: true, data: result, id: result.id }; ··· 5113 5079 const result = dsTagItemAndPublish(args.addressId, args.tagId); 5114 5080 if (!result.alreadyExists) { 5115 5081 const tag = getDb().prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 5116 - publish('system', scopes.GLOBAL, 'tag:address-added', { 5082 + publish('system', 'tag:address-added', { 5117 5083 tagId: args.tagId, tagName: tag?.name, addressId: args.addressId, 5118 5084 }); 5119 5085 } ··· 5136 5102 const tag = getDb().prepare('SELECT name FROM tags WHERE id = ?').get(args.tagId) as { name: string } | undefined; 5137 5103 const result = dsUntagItem(args.addressId, args.tagId); 5138 5104 if (result) { 5139 - publish('system', scopes.GLOBAL, 'tag:address-removed', { 5105 + publish('system', 'tag:address-removed', { 5140 5106 tagId: args.tagId, tagName: tag?.name, addressId: args.addressId, 5141 5107 }); 5142 - publish('system', scopes.GLOBAL, 'tag:item-removed', { 5108 + publish('system', 'tag:item-removed', { 5143 5109 tagId: args.tagId, tagName: tag?.name, itemId: args.addressId, 5144 5110 }); 5145 5111 } ··· 5673 5639 ); 5674 5640 5675 5641 // Publish event 5676 - publish('feature-registry', scopes.GLOBAL, 'feature:updated', { 5642 + publish('feature-registry', 'feature:updated', { 5677 5643 id: args.id, 5678 5644 name: entry.name, 5679 5645 previousVersion: currentVersion, ··· 5859 5825 id: '${featureId}', 5860 5826 registeredTopics: Object.keys(window._cmdHandlers || {}).map(n => \`cmd:execute:\${n}\`), 5861 5827 manifest: { id: '${featureId}', version: '0.1.0' } 5862 - }, api.scopes.SYSTEM); 5828 + }); 5863 5829 5864 5830 api.subscribe('app:shutdown', () => { 5865 5831 if (uninit) uninit(); 5866 - }, api.scopes.SYSTEM); 5832 + }); 5867 5833 } 5868 5834 <${'/'}script> 5869 5835 </body> ··· 6074 6040 writeFeatureHistory(featureId, 'install', '0.1.0', 'dev'); 6075 6041 6076 6042 // Publish installed event 6077 - publish('feature-registry', scopes.GLOBAL, 'feature:installed', { 6043 + publish('feature-registry', 'feature:installed', { 6078 6044 id: featureId, 6079 6045 name: featureName, 6080 6046 source: { type: 'dev' },
+8 -9
backend/electron/tile-launcher.test.ts
··· 15 15 publish, 16 16 unsubscribeAllByPrefix, 17 17 hasSubscriber, 18 - scopes, 19 18 } from './pubsub.js'; 20 19 import { 21 20 shutdownTile, ··· 29 28 30 29 describe('unsubscribeAllByPrefix', () => { 31 30 it('removes every subscription whose source starts with the prefix', () => { 32 - subscribe('peek://mytile/background.html', scopes.GLOBAL, 'test:prefix:a', () => {}); 33 - subscribe('peek://mytile/home.html', scopes.GLOBAL, 'test:prefix:a', () => {}); 34 - subscribe('peek://mytile/home.html', scopes.GLOBAL, 'test:prefix:b', () => {}); 35 - subscribe('peek://othertile/home.html', scopes.GLOBAL, 'test:prefix:a', () => {}); 31 + subscribe('peek://mytile/background.html', 'test:prefix:a', () => {}); 32 + subscribe('peek://mytile/home.html', 'test:prefix:a', () => {}); 33 + subscribe('peek://mytile/home.html', 'test:prefix:b', () => {}); 34 + subscribe('peek://othertile/home.html', 'test:prefix:a', () => {}); 36 35 37 36 const removed = unsubscribeAllByPrefix('peek://mytile/'); 38 37 ··· 52 51 }); 53 52 54 53 it('returns 0 and is a no-op for empty prefix', () => { 55 - subscribe('peek://foo/x', scopes.GLOBAL, 'test:prefix:empty', () => {}); 54 + subscribe('peek://foo/x', 'test:prefix:empty', () => {}); 56 55 const removed = unsubscribeAllByPrefix(''); 57 56 assert.strictEqual(removed, 0); 58 57 // Still subscribed. ··· 62 61 63 62 it('stops deliveries after prefix unsubscribe', () => { 64 63 let count = 0; 65 - subscribe('peek://quiet/home.html', scopes.GLOBAL, 'test:prefix:delivery', () => { count++; }); 66 - publish('peek://src/', scopes.GLOBAL, 'test:prefix:delivery', {}); 64 + subscribe('peek://quiet/home.html', 'test:prefix:delivery', () => { count++; }); 65 + publish('peek://src/', 'test:prefix:delivery', {}); 67 66 assert.strictEqual(count, 1); 68 67 unsubscribeAllByPrefix('peek://quiet/'); 69 - publish('peek://src/', scopes.GLOBAL, 'test:prefix:delivery', {}); 68 + publish('peek://src/', 'test:prefix:delivery', {}); 70 69 assert.strictEqual(count, 1, 'no further deliveries after prefix unsubscribe'); 71 70 }); 72 71 });
+2 -2
backend/electron/tile-launcher.ts
··· 45 45 clearAllTokens, 46 46 setTokenOwner, 47 47 } from './tile-tokens.js'; 48 - import { scopes, publish, getSystemAddress, unsubscribeAll } from './pubsub.js'; 48 + import { publish, getSystemAddress, unsubscribeAll } from './pubsub.js'; 49 49 import { DEBUG, getTilePreloadPath } from './config.js'; 50 50 import { loadSchemaDefaults } from './tile-settings-defaults.js'; 51 51 import { getExtensionPath } from './protocol.js'; ··· 313 313 } 314 314 if (publishCrashEvent) { 315 315 try { 316 - publish(getSystemAddress(), scopes.GLOBAL, 'tile:crashed', { 316 + publish(getSystemAddress(), 'tile:crashed', { 317 317 tileId, entryId, 318 318 reason: details.reason, 319 319 exitCode: details.exitCode,
+18 -18
backend/electron/tile-lazy-events.test.ts
··· 12 12 import * as assert from 'node:assert'; 13 13 14 14 import { 15 - publish, subscribe, unsubscribe, scopes, 15 + publish, subscribe, unsubscribe, 16 16 __clearPrePublishHooksForTest, 17 17 } from './pubsub.js'; 18 18 import { ··· 132 132 133 133 // On launch, simulate the tile registering its real handler and signaling ready. 134 134 launcherState.onLaunch = () => { 135 - subscribe('peek://test-tile-2/bg', scopes.GLOBAL, 'test2:open', () => { 135 + subscribe('peek://test-tile-2/bg', 'test2:open', () => { 136 136 /* real handler */ 137 137 }); 138 138 signalReady(launcherState, 'test-tile-2', 'background'); 139 139 }; 140 140 141 - publish('peek://test-publisher', scopes.GLOBAL, 'test2:open', { id: 'abc' }); 141 + publish('peek://test-publisher', 'test2:open', { id: 'abc' }); 142 142 143 143 // The dispatch hook defers delivery until load+ready. Yield enough 144 144 // microtasks for: launch → microtask that sets loaded + onLaunch → ··· 157 157 158 158 const received: unknown[] = []; 159 159 launcherState.onLaunch = () => { 160 - subscribe('peek://test-tile-3/bg', scopes.GLOBAL, 'test3:open', (msg) => { 160 + subscribe('peek://test-tile-3/bg', 'test3:open', (msg) => { 161 161 received.push(msg); 162 162 }); 163 163 signalReady(launcherState, 'test-tile-3', 'background'); 164 164 }; 165 165 166 166 const payload = { itemId: 'note-42', cursor: { line: 3, col: 5 } }; 167 - publish('peek://test-publisher', scopes.GLOBAL, 'test3:open', payload); 167 + publish('peek://test-publisher', 'test3:open', payload); 168 168 for (let i = 0; i < 10; i++) await flushMicrotasks(); 169 169 170 170 assert.strictEqual(received.length, 1, 'real handler should receive the deferred message'); ··· 179 179 180 180 const received: unknown[] = []; 181 181 launcherState.onLaunch = () => { 182 - subscribe('peek://test-tile-4/bg', scopes.GLOBAL, 'test4:open', (msg) => { 182 + subscribe('peek://test-tile-4/bg', 'test4:open', (msg) => { 183 183 received.push(msg); 184 184 }); 185 185 signalReady(launcherState, 'test-tile-4', 'background'); 186 186 }; 187 187 188 - publish('peek://pub', scopes.GLOBAL, 'test4:open', { n: 1 }); 188 + publish('peek://pub', 'test4:open', { n: 1 }); 189 189 for (let i = 0; i < 10; i++) await flushMicrotasks(); 190 190 191 191 assert.strictEqual(launcherState.launchCalls.length, 1); ··· 193 193 194 194 // Second publish — tile already loaded + ready; hook sees that and 195 195 // continues synchronously. No second launch. 196 - publish('peek://pub', scopes.GLOBAL, 'test4:open', { n: 2 }); 196 + publish('peek://pub', 'test4:open', { n: 2 }); 197 197 for (let i = 0; i < 5; i++) await flushMicrotasks(); 198 198 199 199 assert.strictEqual(launcherState.launchCalls.length, 1, 'tile should not be relaunched'); ··· 209 209 210 210 const received: Array<{ topic: string; msg: unknown }> = []; 211 211 launcherState.onLaunch = () => { 212 - subscribe('peek://test-tile-5/bg', scopes.GLOBAL, 'test5:open', (msg) => { 212 + subscribe('peek://test-tile-5/bg', 'test5:open', (msg) => { 213 213 received.push({ topic: 'test5:open', msg }); 214 214 }); 215 - subscribe('peek://test-tile-5/bg', scopes.GLOBAL, 'test5:add', (msg) => { 215 + subscribe('peek://test-tile-5/bg', 'test5:add', (msg) => { 216 216 received.push({ topic: 'test5:add', msg }); 217 217 }); 218 218 signalReady(launcherState, 'test-tile-5', 'background'); 219 219 }; 220 220 221 221 // First topic triggers load. 222 - publish('peek://pub', scopes.GLOBAL, 'test5:open', { kind: 'open' }); 222 + publish('peek://pub', 'test5:open', { kind: 'open' }); 223 223 for (let i = 0; i < 10; i++) await flushMicrotasks(); 224 224 225 225 assert.strictEqual(launcherState.launchCalls.length, 1); ··· 227 227 assert.strictEqual(received[0].topic, 'test5:open'); 228 228 229 229 // Second topic: tile is already loaded, goes straight to real handler. 230 - publish('peek://pub', scopes.GLOBAL, 'test5:add', { kind: 'add' }); 230 + publish('peek://pub', 'test5:add', { kind: 'add' }); 231 231 for (let i = 0; i < 5; i++) await flushMicrotasks(); 232 232 233 233 assert.strictEqual(launcherState.launchCalls.length, 1, 'tile should not be relaunched'); ··· 246 246 let readyTrigger: (() => void) | null = null; 247 247 const received: unknown[] = []; 248 248 launcherState.onLaunch = () => { 249 - subscribe('peek://test-tile-6/bg', scopes.GLOBAL, 'test6:open', (msg) => { 249 + subscribe('peek://test-tile-6/bg', 'test6:open', (msg) => { 250 250 received.push(msg); 251 251 }); 252 252 readyTrigger = () => signalReady(launcherState, 'test-tile-6', 'background'); 253 253 }; 254 254 255 - publish('peek://pub', scopes.GLOBAL, 'test6:open', { n: 1 }); 255 + publish('peek://pub', 'test6:open', { n: 1 }); 256 256 await flushMicrotasks(); 257 - publish('peek://pub', scopes.GLOBAL, 'test6:open', { n: 2 }); 257 + publish('peek://pub', 'test6:open', { n: 2 }); 258 258 await flushMicrotasks(); 259 - publish('peek://pub', scopes.GLOBAL, 'test6:open', { n: 3 }); 259 + publish('peek://pub', 'test6:open', { n: 3 }); 260 260 await flushMicrotasks(); 261 261 262 262 // All three hooks are awaiting waitForTileReady; none resolved yet. ··· 280 280 registerLazyTile(manifest, '/fake/path', '/fake/preload.js'); 281 281 282 282 const received: unknown[] = []; 283 - subscribe('peek://unrelated', scopes.GLOBAL, 'unrelated:topic', (msg) => { 283 + subscribe('peek://unrelated', 'unrelated:topic', (msg) => { 284 284 received.push(msg); 285 285 }); 286 286 287 - publish('peek://pub', scopes.GLOBAL, 'unrelated:topic', { x: 1 }); 287 + publish('peek://pub', 'unrelated:topic', { x: 1 }); 288 288 // No awaits needed — unmatched topics deliver synchronously. 289 289 // But give the loop a tick just to be safe for any microtask work. 290 290 await flushMicrotasks();
+3 -4
backend/electron/tile-lazy.ts
··· 28 28 */ 29 29 30 30 import { 31 - publish, scopes, registerPrePublishHook, getSystemAddress, 31 + publish, registerPrePublishHook, getSystemAddress, 32 32 type PrePublishHookResult, 33 33 } from './pubsub.js'; 34 34 import { ··· 138 138 if (Array.isArray(manifest.commands) && manifest.commands.length > 0) { 139 139 publish( 140 140 `peek://${tileId}/lazy-stub`, 141 - scopes.GLOBAL, 142 141 'cmd:register-batch', 143 142 { 144 143 commands: manifest.commands.map(cmd => ({ ··· 249 248 // the original publish. 250 249 if (cmd.action?.type === 'window' && cmd.action.url) { 251 250 DEBUG && console.log(`[tile-lazy] Declarative window action for ${name}, opening directly`); 252 - publish(getSystemAddress(), scopes.GLOBAL, 'window:reopen-request', { 251 + publish(getSystemAddress(), 'window:reopen-request', { 253 252 url: cmd.action.url, 254 253 options: { 255 254 ...(cmd.action.options || {}), ··· 259 258 }); 260 259 const typed = msg as { resultTopic?: string } | null; 261 260 if (typed?.resultTopic) { 262 - publish(getSystemAddress(), scopes.GLOBAL, typed.resultTopic, { success: true }); 261 + publish(getSystemAddress(), typed.resultTopic, { success: true }); 263 262 } 264 263 return 'skip'; 265 264 }
+3 -4
backend/electron/tile-lifecycle.test.ts
··· 38 38 publish, 39 39 getSystemAddress, 40 40 __clearPrePublishHooksForTest, 41 - scopes, 42 41 } from './pubsub.js'; 43 42 44 43 // Ensure NODE_ENV is 'test' — resetForTests / __simulateLifecycleReadyForTest ··· 68 67 } { 69 68 const events: StateChangedPayload[] = []; 70 69 const source = `test-state-listener-${Math.random().toString(36).slice(2, 8)}`; 71 - subscribe(source, scopes.GLOBAL, 'tile:state-changed', (msg) => { 70 + subscribe(source, 'tile:state-changed', (msg) => { 72 71 events.push(msg as StateChangedPayload); 73 72 }); 74 73 return { ··· 253 252 // first publishes are never dropped. 254 253 const received: unknown[] = []; 255 254 const source = 'test-core-cmd-registry'; 256 - subscribe(source, scopes.GLOBAL, 'cmd:register', (msg) => { 255 + subscribe(source, 'cmd:register', (msg) => { 257 256 received.push(msg); 258 257 }); 259 258 try { ··· 261 260 // system the publish goes through `publish()` in pubsub.ts. 262 261 // We call the same function directly here — the invariant under 263 262 // test is about ordering, not about which process code path fires. 264 - publish(getSystemAddress(), scopes.GLOBAL, 'cmd:register', { 263 + publish(getSystemAddress(), 'cmd:register', { 265 264 name: 'hello', 266 265 source: 'fx', 267 266 });
+2 -2
backend/electron/tile-lifecycle.ts
··· 35 35 isDispatchable, 36 36 acceptsDynamicRegistration, 37 37 } from './tile-fsm.js'; 38 - import { publish, scopes, getSystemAddress } from './pubsub.js'; 38 + import { publish, getSystemAddress } from './pubsub.js'; 39 39 40 40 // Lazy-load ipcMain via CommonJS require so this module can be imported 41 41 // under ELECTRON_RUN_AS_NODE=1 (where electron's named ESM exports are ··· 264 264 // `tile:state-changed` directly; the authorization-rules table 265 265 // restricts writes to System. 266 266 try { 267 - publish(getSystemAddress(), scopes.GLOBAL, 'tile:state-changed', { 267 + publish(getSystemAddress(), 'tile:state-changed', { 268 268 tileId, 269 269 entryId, 270 270 from,
+8 -20
backend/electron/tile-preload.cts
··· 145 145 } 146 146 }; 147 147 148 - // ── Scopes (always available for reference) ── 149 - 150 - api.scopes = { 151 - SYSTEM: 1, 152 - SELF: 2, 153 - GLOBAL: 3, 154 - }; 155 - 156 148 // ── PubSub (if granted) ── 157 149 // 158 150 // Publish supports two call shapes: 159 - // api.publish(topic, data, scope?) // v1-style 160 - // api.pubsub.publish(topic, data, scope?) // v2-style (kept for clarity) 161 - // Subscribe supports an optional scope tail argument — callers like editor use 162 - // `api.subscribe('editor:open', handler, api.scopes.GLOBAL)`. Scope is only 163 - // advisory for the client — capability-based enforcement happens in main. 151 + // api.publish(topic, data) // v1-style 152 + // api.pubsub.publish(topic, data) // v2-style (kept for clarity) 153 + // 154 + // Scope (SYSTEM / SELF / GLOBAL) was removed in Phase 6 (see 155 + // docs/pubsub-state-machine.md). Privilege is enforced by the 156 + // capability grant's topic allowlist in main. 164 157 165 - async function publishImpl(topic: string, data: unknown, scope?: number) { 158 + async function publishImpl(topic: string, data: unknown) { 166 159 const valid = await validationPromise; 167 160 if (!valid) { 168 161 console.warn('[tile-preload] publish called but token validation failed'); ··· 171 164 ipcRenderer.send('tile:pubsub:publish', { 172 165 token: tileToken, 173 166 source: sourceAddress, 174 - scope: scope || 2, 175 167 topic, 176 168 data, 177 169 }); 178 170 } 179 171 180 - function subscribeImpl(topic: string, callback: (data: unknown) => void, _scope?: number) { 172 + function subscribeImpl(topic: string, callback: (data: unknown) => void) { 181 173 // Attach the IPC listener AND send the upstream subscribe synchronously 182 174 // in the same tick. Deferring the upstream send behind validationPromise 183 175 // created a race: publishers (e.g. tag:item-added from command execution) ··· 339 331 ipcRenderer.send('tile:pubsub:publish', { 340 332 token: tileToken, 341 333 source: sourceAddress, 342 - scope: 3, // GLOBAL 343 334 topic: 'cmd:register', 344 335 data: registerPayload, 345 336 }); ··· 371 362 ipcRenderer.send('tile:pubsub:publish', { 372 363 token: tileToken, 373 364 source: sourceAddress, 374 - scope: 3, 375 365 topic: msg.resultTopic, 376 366 data: error ? { error } : result, 377 367 }); ··· 410 400 ipcRenderer.send('tile:pubsub:publish', { 411 401 token: tileToken, 412 402 source: sourceAddress, 413 - scope: 3, // GLOBAL 414 403 topic: 'cmd:unregister', 415 404 data: { name }, 416 405 }); ··· 463 452 ipcRenderer.send('tile:pubsub:publish', { 464 453 token: tileToken, 465 454 source: sourceAddress, 466 - scope: 3, // GLOBAL 467 455 topic: 'cmd:query-commands', 468 456 data: {}, 469 457 });
+1 -1
backend/electron/tile-strict.test.ts
··· 46 46 47 47 function subscribeViolations(cb: (msg: unknown) => void): () => void { 48 48 const source = `peek://test-violation-subscriber/${Date.now()}`; 49 - pubsub.subscribe(source, pubsub.scopes.GLOBAL, 'tile:capability-violation', cb); 49 + pubsub.subscribe(source, 'tile:capability-violation', cb); 50 50 const unsub = () => pubsub.unsubscribe(source, 'tile:capability-violation'); 51 51 teardowns.push(unsub); 52 52 return unsub;
+2 -2
backend/electron/tile-violations.ts
··· 16 16 * runaway tile retries repeatedly. 17 17 */ 18 18 19 - import { publish, scopes, getSystemAddress } from './pubsub.js'; 19 + import { publish, getSystemAddress } from './pubsub.js'; 20 20 21 21 export interface CapabilityViolationDetails { 22 22 tileId: string | null; ··· 52 52 timestamp: now, 53 53 }; 54 54 55 - publish(getSystemAddress(), scopes.GLOBAL, 'tile:capability-violation', event); 55 + publish(getSystemAddress(), 'tile:capability-violation', event); 56 56 } 57 57 58 58 /**
+2 -2
backend/electron/windows.ts
··· 27 27 getIzuiCoordinator, 28 28 } from './izui-state.js'; 29 29 30 - import { publish, scopes } from './pubsub.js'; 30 + import { publish } from './pubsub.js'; 31 31 32 32 /** 33 33 * Get the appropriate background color based on system theme ··· 185 185 const unhandledAction = escUnhandledPolicy(sessionState, role); 186 186 DEBUG && console.log(`[esc] escUnhandledPolicy(${sessionState}, ${role}) -> ${unhandledAction}`); 187 187 if (unhandledAction === 'open-switcher') { 188 - publish('peek://system/', scopes.GLOBAL, 'cmd:execute:windows', {}); 188 + publish('peek://system/', 'cmd:execute:windows', {}); 189 189 } 190 190 } 191 191 }
+2 -12
backend/types/api.ts
··· 17 17 error?: string; 18 18 } 19 19 20 - // ==================== Scopes ==================== 21 - 22 - export enum ApiScope { 23 - SYSTEM = 1, 24 - SELF = 2, 25 - GLOBAL = 3 26 - } 27 - 28 20 // ==================== Command Scopes ==================== 29 21 30 22 /** ··· 353 345 354 346 export interface IPubSubApi { 355 347 /** Publish a message to a topic */ 356 - publish(topic: string, msg: unknown, scope?: ApiScope): void; 348 + publish(topic: string, msg: unknown): void; 357 349 358 350 /** Subscribe to a topic */ 359 - subscribe(topic: string, callback: (msg: unknown) => void, scope?: ApiScope): void; 351 + subscribe(topic: string, callback: (msg: unknown) => void): void; 360 352 } 361 353 362 354 // ==================== Extensions ==================== ··· 460 452 /** Current debug level */ 461 453 debugLevel: number; 462 454 463 - /** Scope constants for pubsub */ 464 - scopes: typeof ApiScope; 465 455 466 456 /** Keyboard shortcuts */ 467 457 shortcuts: IShortcutsApi;
+2 -2
features/editor/background.js
··· 89 89 params.itemId = msg.itemId; 90 90 } 91 91 openEditor(Object.keys(params).length > 0 ? params : undefined); 92 - }, api.scopes.GLOBAL); 92 + }); 93 93 94 94 // Subscribe to editor:add — open a new blank editor document 95 95 api.pubsub.subscribe('editor:add', () => { 96 96 openEditor(); 97 - }, api.scopes.GLOBAL); 97 + }); 98 98 }, 99 99 100 100 uninit() {
+4 -4
features/editor/home.js
··· 438 438 } catch (err) { 439 439 debug && console.log('[editor] Failed to reload settings:', err); 440 440 } 441 - }, api.scopes.GLOBAL); 441 + }); 442 442 } 443 443 444 444 debug && console.log('[editor] Editor initialized'); ··· 641 641 642 642 // Publish change event 643 643 if (api?.pubsub?.publish) { 644 - api.pubsub.publish('editor:changed', { action: 'update', itemId: currentItemId }, api.scopes.GLOBAL); 644 + api.pubsub.publish('editor:changed', { action: 'update', itemId: currentItemId }); 645 645 } 646 646 } else { 647 647 console.error('[editor] Autosave failed:', result.error); ··· 683 683 684 684 // Notify that a new item was created 685 685 if (api?.pubsub?.publish) { 686 - api.pubsub.publish('editor:changed', { action: 'add', itemId: currentItemId }, api.scopes.GLOBAL); 686 + api.pubsub.publish('editor:changed', { action: 'add', itemId: currentItemId }); 687 687 } 688 688 689 689 // Set context to editor mode ··· 745 745 const handleContentChange = async (content) => { 746 746 // Publish change event for other extensions 747 747 if (api?.pubsub?.publish) { 748 - api.pubsub.publish('editor:contentChanged', { content }, api.scopes.GLOBAL); 748 + api.pubsub.publish('editor:contentChanged', { content }); 749 749 } 750 750 751 751 // If this is a deferred new note and user typed real content, create the item now
+11 -11
features/entities/background.js
··· 246 246 confidence: e.confidence, 247 247 isNew: e.isNew 248 248 })) 249 - }, api.scopes.GLOBAL); 249 + }); 250 250 251 251 // Update context with the latest extracted entities for this window. 252 252 // The `context` capability is declared in the manifest with ··· 307 307 confidence: e.confidence, 308 308 isNew: e.isNew 309 309 })) 310 - }, api.scopes.GLOBAL); 310 + }); 311 311 312 312 return entities; 313 313 } ··· 394 394 if (msg.itemType === 'url') { 395 395 handlePageLoad(msg); 396 396 } 397 - }, api.scopes.GLOBAL); 397 + }); 398 398 399 399 // Listen for page content ready events from the main process. 400 400 // Fires when page-title-updated occurs, meaning the page has loaded ··· 404 404 if (msg.url && msg.url.startsWith('http')) { 405 405 handlePageLoad({ content: msg.url, title: msg.title || '' }); 406 406 } 407 - }, api.scopes.GLOBAL); 407 + }); 408 408 409 409 // Listen for manual extraction requests via pubsub 410 410 api.pubsub.subscribe('entities:extract', async (msg) => { ··· 428 428 confidence: e.confidence, 429 429 isNew: e.isNew 430 430 })) 431 - }, api.scopes.GLOBAL); 431 + }); 432 432 433 433 api.pubsub.publish('entities:extract:response', { 434 434 url: msg.url, 435 435 entities 436 - }, api.scopes.GLOBAL); 436 + }); 437 437 } 438 - }, api.scopes.GLOBAL); 438 + }); 439 439 440 440 // Listen for entity queries 441 441 api.pubsub.subscribe('entities:search', async (msg) => { ··· 447 447 api.pubsub.publish('entities:search:response', { 448 448 query: msg.query, 449 449 entities 450 - }, api.scopes.GLOBAL); 451 - }, api.scopes.GLOBAL); 450 + }); 451 + }); 452 452 453 453 // Listen for get-for-url requests 454 454 api.pubsub.subscribe('entities:get-for-url', async (msg) => { ··· 475 475 api.pubsub.publish('entities:get-for-url:response', { 476 476 url: msg.url, 477 477 entities 478 - }, api.scopes.GLOBAL); 478 + }); 479 479 } 480 - }, api.scopes.GLOBAL); 480 + }); 481 481 482 482 }, 483 483
+16 -16
features/entities/home.js
··· 203 203 confidence: e.confidence, 204 204 isNew: e.isNew 205 205 })) 206 - }, api.scopes.GLOBAL); 206 + }); 207 207 208 208 try { 209 209 await api.context.set('entities', entities.map(e => ({ ··· 253 253 confidence: e.confidence, 254 254 isNew: e.isNew 255 255 })) 256 - }, api.scopes.GLOBAL); 256 + }); 257 257 258 258 return entities; 259 259 } ··· 323 323 if (msg.itemType === 'url') { 324 324 handlePageLoad(msg); 325 325 } 326 - }, api.scopes.GLOBAL); 326 + }); 327 327 328 328 api.pubsub.subscribe('page:content-ready', (msg) => { 329 329 if (msg.url && msg.url.startsWith('http')) { 330 330 handlePageLoad({ content: msg.url, title: msg.title || '' }); 331 331 } 332 - }, api.scopes.GLOBAL); 332 + }); 333 333 334 334 // External API: manual extraction request (used by page widget in app/page/page.js) 335 335 api.pubsub.subscribe('entities:extract', async (msg) => { ··· 351 351 confidence: e.confidence, 352 352 isNew: e.isNew 353 353 })) 354 - }, api.scopes.GLOBAL); 354 + }); 355 355 356 356 api.pubsub.publish('entities:extract:response', { 357 357 url: msg.url, 358 358 entities 359 - }, api.scopes.GLOBAL); 359 + }); 360 360 } 361 - }, api.scopes.GLOBAL); 361 + }); 362 362 363 363 // External API: entity search (used by external consumers) 364 364 api.pubsub.subscribe('entities:search', async (msg) => { ··· 370 370 api.pubsub.publish('entities:search:response', { 371 371 query: msg.query, 372 372 entities 373 - }, api.scopes.GLOBAL); 374 - }, api.scopes.GLOBAL); 373 + }); 374 + }); 375 375 376 376 // External API: get entities for URL (used by page widget in app/page/page.js) 377 377 api.pubsub.subscribe('entities:get-for-url', async (msg) => { ··· 397 397 api.pubsub.publish('entities:get-for-url:response', { 398 398 url: msg.url, 399 399 entities 400 - }, api.scopes.GLOBAL); 400 + }); 401 401 } 402 - }, api.scopes.GLOBAL); 402 + }); 403 403 } 404 404 405 405 function uninitBackground() { ··· 987 987 if (api) { 988 988 api.pubsub.subscribe('entities:extracted', () => { 989 989 if (currentView === VIEW_LIST) renderEntities(); 990 - }, api.scopes.GLOBAL); 990 + }); 991 991 992 992 // Also listen for editor changes that might affect entities 993 993 api.pubsub.subscribe('editor:changed', () => { 994 994 if (currentView === VIEW_LIST) renderEntities(); 995 - }, api.scopes.GLOBAL); 995 + }); 996 996 997 997 // Subscribe to sync and item events for reactive updates 998 998 const debouncedRefresh = (() => { ··· 1007 1007 1008 1008 api.pubsub.subscribe('sync:pull-completed', () => { 1009 1009 debouncedRefresh(); 1010 - }, api.scopes.GLOBAL); 1010 + }); 1011 1011 1012 1012 api.pubsub.subscribe('item:created', () => { 1013 1013 debouncedRefresh(); 1014 - }, api.scopes.GLOBAL); 1014 + }); 1015 1015 1016 1016 api.pubsub.subscribe('item:deleted', () => { 1017 1017 debouncedRefresh(); 1018 - }, api.scopes.GLOBAL); 1018 + }); 1019 1019 1020 1020 // Register escape handler for back-navigation from detail view 1021 1021 if (api.escape) {
+2 -2
features/example/README.md
··· 49 49 used in new code. 50 50 51 51 ```javascript 52 - api.pubsub.publish('example:image-added', payload, api.scopes.GLOBAL); 52 + api.pubsub.publish('example:image-added', payload); 53 53 54 54 api.pubsub.subscribe('example:image-added', (msg) => { 55 55 renderGallery(); 56 - }, api.scopes.GLOBAL); 56 + }); 57 57 ``` 58 58 59 59 The manifest must declare every topic you publish or subscribe to under
+1 -1
features/example/background.js
··· 87 87 88 88 // Notify that a new image was added 89 89 if (hasPeekAPI) { 90 - api.pubsub.publish('example:image-added', { id: imageId, ...imageData }, api.scopes.GLOBAL); 90 + api.pubsub.publish('example:image-added', { id: imageId, ...imageData }); 91 91 } 92 92 93 93 // Open the gallery to show the new image
+1 -1
features/example/gallery.html
··· 364 364 api.pubsub.subscribe('example:image-added', (msg) => { 365 365 console.log('[gallery] Image added event:', msg); 366 366 renderGallery(); 367 - }, api.scopes.GLOBAL); 367 + }); 368 368 } 369 369 370 370 // Initial render
+17 -17
features/features-manager/background.js
··· 147 147 if (!msg.id) return; 148 148 try { 149 149 const result = await api.features.disable(msg.id); 150 - api.pubsub.publish('feature:disable-result', { id: msg.id, ...result }, api.scopes.GLOBAL); 150 + api.pubsub.publish('feature:disable-result', { id: msg.id, ...result }); 151 151 } catch (err) { 152 - api.pubsub.publish('feature:disable-result', { id: msg.id, error: err.message }, api.scopes.GLOBAL); 152 + api.pubsub.publish('feature:disable-result', { id: msg.id, error: err.message }); 153 153 } 154 - }, api.scopes.GLOBAL); 154 + }); 155 155 156 156 api.pubsub.subscribe('feature:enable-request', async (msg) => { 157 157 if (!msg.id) return; 158 158 try { 159 159 const result = await api.features.enable(msg.id); 160 - api.pubsub.publish('feature:enable-result', { id: msg.id, ...result }, api.scopes.GLOBAL); 160 + api.pubsub.publish('feature:enable-result', { id: msg.id, ...result }); 161 161 } catch (err) { 162 - api.pubsub.publish('feature:enable-result', { id: msg.id, error: err.message }, api.scopes.GLOBAL); 162 + api.pubsub.publish('feature:enable-result', { id: msg.id, error: err.message }); 163 163 } 164 - }, api.scopes.GLOBAL); 164 + }); 165 165 166 166 api.pubsub.subscribe('feature:remove-request', async (msg) => { 167 167 if (!msg.id) return; 168 168 try { 169 169 const result = await api.features.remove(msg.id); 170 - api.pubsub.publish('feature:remove-result', { id: msg.id, ...result }, api.scopes.GLOBAL); 170 + api.pubsub.publish('feature:remove-result', { id: msg.id, ...result }); 171 171 } catch (err) { 172 - api.pubsub.publish('feature:remove-result', { id: msg.id, error: err.message }, api.scopes.GLOBAL); 172 + api.pubsub.publish('feature:remove-result', { id: msg.id, error: err.message }); 173 173 } 174 - }, api.scopes.GLOBAL); 174 + }); 175 175 176 176 // Listen for lex session changes (used by publish flow) 177 177 api.pubsub.subscribe('lex:session-changed', (msg) => { ··· 182 182 currentLexSession = null; 183 183 console.log('[features-manager] Lex session cleared'); 184 184 } 185 - }, api.scopes.GLOBAL); 185 + }); 186 186 187 187 // ── Update Checking ── 188 188 ··· 235 235 capabilitiesChanged: true, 236 236 added: applyResult.added, 237 237 removed: applyResult.removed, 238 - }, api.scopes.GLOBAL); 238 + }); 239 239 } else if (applyResult.error && !applyResult.capabilitiesChanged) { 240 240 console.error(`[features-manager] Auto-update failed for ${update.id}:`, applyResult.error); 241 241 } else if (applyResult.success) { ··· 248 248 currentVersion: update.currentVersion, 249 249 availableVersion: update.availableVersion, 250 250 capabilitiesChanged: false, 251 - }, api.scopes.GLOBAL); 251 + }); 252 252 } 253 253 } 254 254 ··· 372 372 atUri: msg.atUri, 373 373 success: false, 374 374 error: resolveResult.error, 375 - }, api.scopes.GLOBAL); 375 + }); 376 376 return; 377 377 } 378 378 ··· 385 385 success: !installResult.error, 386 386 entry: installResult.entry, 387 387 error: installResult.error, 388 - }, api.scopes.GLOBAL); 388 + }); 389 389 return; 390 390 } 391 391 ··· 396 396 phase: 'resolved', 397 397 manifest: resolveResult.manifest, 398 398 source: resolveResult.source, 399 - }, api.scopes.GLOBAL); 399 + }); 400 400 401 401 } catch (err) { 402 402 console.error('[features-manager] install-request error:', err); ··· 404 404 atUri: msg.atUri, 405 405 success: false, 406 406 error: err.message, 407 - }, api.scopes.GLOBAL); 407 + }); 408 408 } 409 - }, api.scopes.GLOBAL); 409 + }); 410 410 }, 411 411 412 412 uninit() {
+1 -1
features/features-manager/browse.js
··· 516 516 api.pubsub.subscribe('feature:installed', async () => { 517 517 await loadInstalledFeatures(); 518 518 renderResults(); 519 - }, api.scopes?.GLOBAL); 519 + }); 520 520 } 521 521 522 522 // ─── Init ───────────────────────────────────────────────────────────
+1 -1
features/features-manager/manage.js
··· 983 983 for (const topic of refreshTopics) { 984 984 api.pubsub.subscribe(topic, () => { 985 985 loadFeatures(); 986 - }, api.scopes?.GLOBAL); 986 + }); 987 987 } 988 988 } 989 989
+2 -2
features/features-manager/publish.js
··· 410 410 function onLexSessionRefreshed(updated) { 411 411 lexSession = updated; 412 412 try { 413 - api.pubsub.publish('lex:session-changed', { session: updated }, api.scopes?.GLOBAL); 413 + api.pubsub.publish('lex:session-changed', { session: updated }); 414 414 } catch {} 415 415 } 416 416 ··· 799 799 if (currentStep === 3) { 800 800 renderStep3(); 801 801 } 802 - }, api.scopes?.GLOBAL); 802 + }); 803 803 } 804 804 805 805 // ─── Init ──────────────────────────────────────────────────────────
+1 -1
features/feeds/background.js
··· 386 386 await pollFeed(msg.itemId, item.data.content); 387 387 } 388 388 } 389 - }, api.scopes.GLOBAL); 389 + }); 390 390 391 391 }, 392 392
+1 -1
features/files/background.js
··· 535 535 } 536 536 537 537 // Notify editor of the new item 538 - api.publish('editor:changed', { action: 'add', itemId }, api.scopes.GLOBAL); 538 + api.publish('editor:changed', { action: 'add', itemId }); 539 539 540 540 return { 541 541 success: true,
+26 -26
features/groups/home.js
··· 619 619 await savePinnedItems(groupId, pins); 620 620 } 621 621 622 - api.pubsub.publish('group:pin-changed', { groupId, itemId: item.id, pinned: true }, api.scopes.GLOBAL); 622 + api.pubsub.publish('group:pin-changed', { groupId, itemId: item.id, pinned: true }); 623 623 console.log(`[ext:groups] Pinned item "${url}" in group ${groupId}`); 624 624 return { success: true, itemId: item.id, groupId }; 625 625 }; ··· 642 642 await savePinnedItems(groupId, pins); 643 643 } 644 644 645 - api.pubsub.publish('group:pin-changed', { groupId, itemId: item.id, pinned: false }, api.scopes.GLOBAL); 645 + api.pubsub.publish('group:pin-changed', { groupId, itemId: item.id, pinned: false }); 646 646 console.log(`[ext:groups] Unpinned item "${url}" from group ${groupId}`); 647 647 return { success: true, itemId: item.id, groupId }; 648 648 }; ··· 881 881 api.pubsub.subscribe('cmd:execute:pin', async (msg) => { 882 882 const result = await pinItem(msg.search?.trim()); 883 883 if (msg.expectResult && msg.resultTopic) { 884 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 884 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 885 885 } 886 - }, api.scopes.GLOBAL); 886 + }); 887 887 api.pubsub.publish('cmd:register', { 888 888 name: 'pin', 889 889 description: 'Pin a URL in the current group (always opens with group)', ··· 892 892 accepts: [], 893 893 produces: [], 894 894 params: [{ name: 'url', type: 'string', required: true, description: 'URL to pin' }] 895 - }, api.scopes.GLOBAL); 895 + }); 896 896 897 897 // unpin <url> 898 898 api.pubsub.subscribe('cmd:execute:unpin', async (msg) => { 899 899 const result = await unpinItem(msg.search?.trim()); 900 900 if (msg.expectResult && msg.resultTopic) { 901 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 901 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 902 902 } 903 - }, api.scopes.GLOBAL); 903 + }); 904 904 api.pubsub.publish('cmd:register', { 905 905 name: 'unpin', 906 906 description: 'Unpin a URL from the current group', ··· 909 909 accepts: [], 910 910 produces: [], 911 911 params: [{ name: 'url', type: 'string', required: true, description: 'URL to unpin' }] 912 - }, api.scopes.GLOBAL); 912 + }); 913 913 914 914 console.log('[ext:groups] Standalone commands registered: pin, unpin'); 915 915 }; ··· 917 917 const uninitCommands = () => { 918 918 unregisterNoun('groups'); 919 919 for (const name of ['pin', 'unpin']) { 920 - api.pubsub.publish('cmd:unregister', { name }, api.scopes.GLOBAL); 920 + api.pubsub.publish('cmd:unregister', { name }); 921 921 } 922 922 console.log('[ext:groups] Noun and commands unregistered: groups'); 923 923 }; ··· 934 934 debug && console.log('[ext:groups] Groups window closed, keeping group mode on member windows'); 935 935 groupsWindowId = null; 936 936 } 937 - }, api.scopes.GLOBAL); 937 + }); 938 938 939 939 api.pubsub.subscribe('groups:settings-changed', async () => { 940 940 console.log('[ext:groups] settings changed, reinitializing'); ··· 942 942 currentSettings = await loadSettings(); 943 943 initShortcut(currentSettings.prefs.shortcutKey); 944 944 initCommands(); 945 - }, api.scopes.GLOBAL); 945 + }); 946 946 947 947 api.pubsub.subscribe('groups:settings-update', async (msg) => { 948 948 console.log('[ext:groups] settings-update received:', msg); ··· 959 959 uninitBackground(); 960 960 initShortcut(currentSettings.prefs.shortcutKey); 961 961 initCommands(); 962 - api.pubsub.publish('groups:settings-changed', currentSettings, api.scopes.GLOBAL); 962 + api.pubsub.publish('groups:settings-changed', currentSettings); 963 963 } catch (err) { 964 964 console.error('[ext:groups] settings-update error:', err); 965 965 } 966 - }, api.scopes.GLOBAL); 966 + }); 967 967 }; 968 968 969 969 const uninitBackground = () => { ··· 1050 1050 api.pubsub.subscribe('tag:item-added', (msg) => { 1051 1051 debug && console.log('[groups] tag:item-added event received:', msg); 1052 1052 debouncedRefresh(); 1053 - }, api.scopes.GLOBAL); 1053 + }); 1054 1054 1055 1055 api.pubsub.subscribe('tag:item-removed', (msg) => { 1056 1056 debug && console.log('[groups] tag:item-removed event received:', msg); 1057 1057 debouncedRefresh(); 1058 - }, api.scopes.GLOBAL); 1058 + }); 1059 1059 1060 1060 api.pubsub.subscribe('tag:created', (msg) => { 1061 1061 debug && console.log('[groups] tag:created event received:', msg); 1062 1062 debouncedRefresh(); 1063 - }, api.scopes.GLOBAL); 1063 + }); 1064 1064 1065 1065 // Subscribe to item events for reactive updates 1066 1066 api.pubsub.subscribe('item:created', (msg) => { 1067 1067 debug && console.log('[groups] item:created event received:', msg); 1068 1068 debouncedRefresh(); 1069 - }, api.scopes.GLOBAL); 1069 + }); 1070 1070 1071 1071 api.pubsub.subscribe('item:deleted', (msg) => { 1072 1072 debug && console.log('[groups] item:deleted event received:', msg); 1073 1073 debouncedRefresh(); 1074 - }, api.scopes.GLOBAL); 1074 + }); 1075 1075 1076 1076 // Subscribe to sync events — sync operations bypass per-item events, 1077 1077 // so we need this to refresh when new data arrives via sync 1078 1078 api.pubsub.subscribe('sync:pull-completed', (msg) => { 1079 1079 debug && console.log('[groups] sync:pull-completed event received:', msg); 1080 1080 debouncedRefresh(); 1081 - }, api.scopes.GLOBAL); 1081 + }); 1082 1082 1083 1083 // Subscribe to tag rename events for reactive updates 1084 1084 api.pubsub.subscribe('tag:renamed', (msg) => { 1085 1085 debug && console.log('[groups] tag:renamed event received:', msg); 1086 1086 debouncedRefresh(); 1087 - }, api.scopes.GLOBAL); 1087 + }); 1088 1088 1089 1089 // Subscribe to pin change events for reactive updates 1090 1090 api.pubsub.subscribe('group:pin-changed', (msg) => { 1091 1091 debug && console.log('[groups] group:pin-changed event received:', msg); 1092 1092 debouncedRefresh(); 1093 - }, api.scopes.GLOBAL); 1093 + }); 1094 1094 1095 1095 // Subscribe to color changes (e.g. border color resolution persisted back) 1096 1096 api.pubsub.subscribe('tag:color-changed', (msg) => { 1097 1097 debug && console.log('[groups] tag:color-changed event received:', msg); 1098 1098 debouncedRefresh(); 1099 - }, api.scopes.GLOBAL); 1099 + }); 1100 1100 1101 1101 // Set up create group UI 1102 1102 setupCreateGroup(); ··· 1827 1827 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 1828 1828 try { 1829 1829 await api.datastore.deleteItem(item.id); 1830 - api.pubsub.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 1830 + api.pubsub.publish('item:deleted', { id: item.id }); 1831 1831 if (state.currentTag) { await loadAddressesForTag(state.currentTag.id); renderAddresses(); } 1832 1832 } catch (err) { 1833 1833 console.error('[groups] Failed to delete item:', err); ··· 1836 1836 onTagRemove: async (item, tag) => { 1837 1837 try { 1838 1838 await api.datastore.untagItem(item.id, tag.id); 1839 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 1839 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }); 1840 1840 if (state.currentTag) { await loadAddressesForTag(state.currentTag.id); renderAddresses(); } 1841 1841 } catch (err) { 1842 1842 console.error('[groups] Failed to remove tag:', err); ··· 1894 1894 e.stopPropagation(); 1895 1895 try { 1896 1896 await api.datastore.untagItem(address.id, state.currentTag.id); 1897 - api.pubsub.publish('tag:item-removed', { itemId: address.id, tagId: state.currentTag.id }, api.scopes.GLOBAL); 1897 + api.pubsub.publish('tag:item-removed', { itemId: address.id, tagId: state.currentTag.id }); 1898 1898 if (state.currentTag) { await loadAddressesForTag(state.currentTag.id); renderAddresses(); } 1899 1899 } catch (err) { 1900 1900 console.error('[groups] Failed to remove item from group:', err); ··· 1974 1974 chip.remove(); 1975 1975 try { 1976 1976 await api.datastore.untagItem(address.id, tag.id); 1977 - api.pubsub.publish('tag:item-removed', { itemId: address.id, tagId: tag.id }, api.scopes.GLOBAL); 1977 + api.pubsub.publish('tag:item-removed', { itemId: address.id, tagId: tag.id }); 1978 1978 } catch (err) { 1979 1979 console.error('[groups] Failed to remove tag:', err); 1980 1980 }
+3 -4
features/hello-world/home.html
··· 154 154 log('api.publish is function: ' + (typeof api.publish === 'function'), typeof api.publish === 'function' ? 'ok' : 'fail'); 155 155 log('api.datastore present: ' + !!api.datastore, api.datastore ? 'ok' : 'fail'); 156 156 log('api.datastore.queryItems is function: ' + (typeof api.datastore?.queryItems === 'function'), typeof api.datastore?.queryItems === 'function' ? 'ok' : 'fail'); 157 - log('api.scopes present: ' + !!api.scopes, api.scopes ? 'ok' : 'fail'); 158 157 log('api.getTileId: ' + (api.getTileId ? api.getTileId() : 'n/a')); 159 158 160 159 // ── Phase 2: initialize ── ··· 210 209 try { 211 210 const unsub = api.pubsub.subscribe('item:created', (msg) => { 212 211 log('event item:created: ' + JSON.stringify(msg).slice(0, 200), 'ok'); 213 - }, api.scopes?.GLOBAL); 212 + }); 214 213 log('pubsub.subscribe returned: ' + (typeof unsub), typeof unsub === 'function' ? 'ok' : 'warn'); 215 214 } catch (err) { 216 215 log('pubsub.subscribe THREW: ' + (err.message || err), 'fail'); ··· 223 222 try { 224 223 api.subscribe('hello:ping', (msg) => { 225 224 log('event hello:ping: ' + JSON.stringify(msg).slice(0, 200), 'ok'); 226 - }, api.scopes?.GLOBAL); 225 + }); 227 226 log('api.subscribe(hello:ping) registered', 'ok'); 228 227 } catch (err) { 229 228 log('api.subscribe THREW: ' + (err.message || err), 'fail'); ··· 234 233 header('pubsub publish'); 235 234 if (api.pubsub?.publish) { 236 235 try { 237 - api.pubsub.publish('hello:ping', { ts: Date.now() }, api.scopes?.GLOBAL); 236 + api.pubsub.publish('hello:ping', { ts: Date.now() }); 238 237 log('pubsub.publish hello:ping fired', 'ok'); 239 238 } catch (err) { 240 239 log('pubsub.publish THREW: ' + (err.message || err), 'fail');
+1 -1
features/lex/background.js
··· 286 286 knownCollectionNsids = []; 287 287 console.log('[lex:bg] Session cleared'); 288 288 } 289 - }, api.scopes.GLOBAL); 289 + }); 290 290 }, 291 291 292 292 uninit() {
+3 -3
features/lex/chain-form.js
··· 31 31 const api = window.app; 32 32 33 33 function emitChannel(topic, data) { 34 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 34 + api.pubsub.publish(topic, data); 35 35 } 36 36 37 37 // Read URL params ··· 119 119 data: data || null, 120 120 mimeType: 'application/json', 121 121 done: true, 122 - }, api.scopes.GLOBAL); 122 + }); 123 123 } 124 124 125 125 /** ··· 323 323 } 324 324 325 325 loadForm(nsid); 326 - }, api.scopes.GLOBAL); 326 + }); 327 327 } 328 328 329 329 init();
+3 -3
features/lex/home.js
··· 170 170 171 171 function onRecentLexiconsChanged() { 172 172 // Also publish for chain-form.js (runs in separate modal window) 173 - api.pubsub.publish('lex:recent-lexicons-changed', { nsids: state.recentLexicons }, api.scopes.GLOBAL); 173 + api.pubsub.publish('lex:recent-lexicons-changed', { nsids: state.recentLexicons }); 174 174 } 175 175 176 176 // Listen for recent lexicon changes from chain-form.js ··· 188 188 saveRecentLexicons(); 189 189 renderRecentLexicons(); 190 190 } 191 - }, api.scopes.GLOBAL); 191 + }); 192 192 193 193 // ============================================================================ 194 194 // Session state ··· 760 760 } else { 761 761 switchPanel('create'); 762 762 } 763 - }, api.scopes.GLOBAL); 763 + }); 764 764 765 765 // Load session from storage directly 766 766 await loadSession();
+3 -3
features/lists/background.html
··· 32 32 labels: extension.labels, 33 33 version: '1.0.0' 34 34 } 35 - }, api.scopes.SYSTEM); 35 + }); 36 36 37 37 // Handle shutdown request from main process 38 38 api.onShutdown(() => { ··· 48 48 if (extension.uninit) { 49 49 extension.uninit(); 50 50 } 51 - }, api.scopes.SYSTEM); 51 + }); 52 52 53 53 // Handle extension-specific shutdown 54 54 api.pubsub.subscribe(`ext:${extId}:shutdown`, () => { ··· 56 56 if (extension.uninit) { 57 57 extension.uninit(); 58 58 } 59 - }, api.scopes.SYSTEM); 59 + }); 60 60 </script> 61 61 </body> 62 62 </html>
+3 -3
features/lists/background.js
··· 112 112 uninitShortcut(); 113 113 currentSettings = await loadSettings(); 114 114 initShortcut(currentSettings.prefs.shortcutKey); 115 - }, api.scopes.GLOBAL); 115 + }); 116 116 117 117 // Listen for settings updates from Settings UI 118 118 api.pubsub.subscribe('lists:settings-update', async (msg) => { ··· 135 135 uninitShortcut(); 136 136 initShortcut(currentSettings.prefs.shortcutKey); 137 137 138 - api.pubsub.publish('lists:settings-changed', currentSettings, api.scopes.GLOBAL); 138 + api.pubsub.publish('lists:settings-changed', currentSettings); 139 139 } catch (err) { 140 140 console.error('[ext:lists] settings-update error:', err); 141 141 } 142 - }, api.scopes.GLOBAL); 142 + }); 143 143 }; 144 144 145 145 const uninitShortcut = () => {
+9 -9
features/lists/home.js
··· 339 339 onDelete: async (item) => { 340 340 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 341 341 await api.datastore.deleteItem(item.id); 342 - api.pubsub.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 342 + api.pubsub.publish('item:deleted', { id: item.id }); 343 343 }, 344 344 onTagRemove: async (item, tag) => { 345 345 await api.datastore.untagItem(item.id, tag.id); 346 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 346 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }); 347 347 }, 348 348 onClick: async (item) => { 349 349 state.selectedIndex = index; ··· 379 379 } 380 380 } else if (item.type === 'tag') { 381 381 // Open the tags view filtered to this tag 382 - api.pubsub.publish('editor:open', { itemId: item.id }, api.scopes.GLOBAL); 382 + api.pubsub.publish('editor:open', { itemId: item.id }); 383 383 } else if (item.type === 'text' || item.type === 'tagset') { 384 384 // Open in editor 385 - api.pubsub.publish('editor:open', { itemId: item.id }, api.scopes.GLOBAL); 385 + api.pubsub.publish('editor:open', { itemId: item.id }); 386 386 } 387 387 }; 388 388 ··· 533 533 api.pubsub.subscribe('sync:pull-completed', (msg) => { 534 534 debug && console.log('[lists] sync:pull-completed event received:', msg); 535 535 debouncedRefresh(); 536 - }, api.scopes.GLOBAL); 536 + }); 537 537 538 538 api.pubsub.subscribe('item:created', (msg) => { 539 539 debug && console.log('[lists] item:created event received:', msg); 540 540 debouncedRefresh(); 541 - }, api.scopes.GLOBAL); 541 + }); 542 542 543 543 api.pubsub.subscribe('item:deleted', (msg) => { 544 544 debug && console.log('[lists] item:deleted event received:', msg); 545 545 debouncedRefresh(); 546 - }, api.scopes.GLOBAL); 546 + }); 547 547 548 548 api.pubsub.subscribe('tag:item-added', (msg) => { 549 549 debug && console.log('[lists] tag:item-added event received:', msg); 550 550 debouncedRefresh(); 551 - }, api.scopes.GLOBAL); 551 + }); 552 552 553 553 api.pubsub.subscribe('tag:item-removed', (msg) => { 554 554 debug && console.log('[lists] tag:item-removed event received:', msg); 555 555 debouncedRefresh(); 556 - }, api.scopes.GLOBAL); 556 + }); 557 557 558 558 // Initialize when DOM is ready 559 559 document.addEventListener('DOMContentLoaded', init);
+4 -4
features/pagestream/background.js
··· 142 142 debug && console.log('[ext:pagestream] Pagestream window closed'); 143 143 pagestreamWindowId = null; 144 144 } 145 - }, api.scopes.GLOBAL); 145 + }); 146 146 147 147 // Listen for settings changes to hot-reload 148 148 api.pubsub.subscribe('pagestream:settings-changed', async () => { ··· 151 151 currentSettings = await loadSettings(); 152 152 initShortcut(currentSettings.prefs.shortcutKey); 153 153 initCommands(); 154 - }, api.scopes.GLOBAL); 154 + }); 155 155 156 156 // Listen for settings updates from Settings UI 157 157 api.pubsub.subscribe('pagestream:settings-update', async (msg) => { ··· 175 175 initShortcut(currentSettings.prefs.shortcutKey); 176 176 initCommands(); 177 177 178 - api.pubsub.publish('pagestream:settings-changed', currentSettings, api.scopes.GLOBAL); 178 + api.pubsub.publish('pagestream:settings-changed', currentSettings); 179 179 } catch (err) { 180 180 console.error('[ext:pagestream] settings-update error:', err); 181 181 } 182 - }, api.scopes.GLOBAL); 182 + }); 183 183 }; 184 184 185 185 const uninit = () => {
+11 -11
features/pagestream/home.js
··· 419 419 api.pubsub.publish('page:navigate', { 420 420 windowId: state.openWindowId, 421 421 url: entry.item.content 422 - }, api.scopes.GLOBAL); 422 + }); 423 423 } 424 424 } 425 425 }; ··· 429 429 // is safe to call any time, it just no-ops if nothing is open) 430 430 api.pubsub.subscribe('pagestream:nav', (msg) => { 431 431 if (msg.action) moveSelection(msg.action); 432 - }, api.scopes.GLOBAL); 432 + }); 433 433 434 434 const onPageHostClosed = async (closedWindowId) => { 435 435 if (closedWindowId !== state.openWindowId) return; ··· 477 477 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 478 478 try { 479 479 await api.datastore.deleteItem(item.id); 480 - api.pubsub.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 480 + api.pubsub.publish('item:deleted', { id: item.id }); 481 481 } catch (err) { 482 482 console.error('[pagestream] Failed to delete item:', err); 483 483 } ··· 485 485 onTagRemove: async (item, tag) => { 486 486 try { 487 487 await api.datastore.untagItem(item.id, tag.id); 488 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 488 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }); 489 489 } catch (err) { 490 490 console.error('[pagestream] Failed to remove tag:', err); 491 491 } ··· 573 573 chip.remove(); 574 574 try { 575 575 await api.datastore.untagItem(item.id, tag.id); 576 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 576 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }); 577 577 } catch (err) { 578 578 console.error('[pagestream] Failed to remove tag:', err); 579 579 } ··· 706 706 api.pubsub.subscribe('window:closed', (msg) => { 707 707 const closedId = msg?.id; 708 708 if (closedId) onPageHostClosed(closedId); 709 - }, api.scopes.GLOBAL); 709 + }); 710 710 711 - api.pubsub.subscribe('item:created', () => debouncedRefresh(), api.scopes.GLOBAL); 712 - api.pubsub.subscribe('item:deleted', () => debouncedRefresh(), api.scopes.GLOBAL); 713 - api.pubsub.subscribe('tag:item-added', () => debouncedRefresh(), api.scopes.GLOBAL); 714 - api.pubsub.subscribe('tag:item-removed', () => debouncedRefresh(), api.scopes.GLOBAL); 715 - api.pubsub.subscribe('sync:pull-completed', () => debouncedRefresh(), api.scopes.GLOBAL); 711 + api.pubsub.subscribe('item:created', () => debouncedRefresh()); 712 + api.pubsub.subscribe('item:deleted', () => debouncedRefresh()); 713 + api.pubsub.subscribe('tag:item-added', () => debouncedRefresh()); 714 + api.pubsub.subscribe('tag:item-removed', () => debouncedRefresh()); 715 + api.pubsub.subscribe('sync:pull-completed', () => debouncedRefresh()); 716 716 717 717 setInterval(() => { 718 718 const timeElements = document.querySelectorAll('.card-time');
+9 -9
features/pagewidgets-sample/background.js
··· 56 56 } else { 57 57 reject(new Error(msg.error || 'Script execution failed')); 58 58 } 59 - }, api.scopes.GLOBAL); 59 + }); 60 60 61 61 // Set timeout 62 62 timer = setTimeout(() => { ··· 69 69 extensionId: EXTENSION_ID, 70 70 requestId, 71 71 script, 72 - }, api.scopes.GLOBAL); 72 + }); 73 73 }); 74 74 } 75 75 ··· 184 184 widgetId: WIDGET_ID, 185 185 title: 'Page Summary', 186 186 html, 187 - }, api.scopes.GLOBAL); 187 + }); 188 188 189 189 debug && console.log('[ext:pagewidgets-sample] Widget rendered for:', msg.url); 190 190 } catch (err) { ··· 195 195 widgetId: WIDGET_ID, 196 196 title: 'Page Summary', 197 197 html: '<div style="font-size:12px;color:var(--theme-text-muted,#666);font-style:italic;">Could not extract page metadata</div>', 198 - }, api.scopes.GLOBAL); 198 + }); 199 199 } 200 200 } 201 201 ··· 216 216 api.pubsub.publish('widget:close', { 217 217 extensionId: EXTENSION_ID, 218 218 widgetId: WIDGET_ID, 219 - }, api.scopes.GLOBAL); 219 + }); 220 220 } 221 221 222 222 // ===== Lifecycle ===== ··· 230 230 widgetId: WIDGET_ID, 231 231 title: 'Page Summary', 232 232 position: 'right', 233 - }, api.scopes.GLOBAL); 233 + }); 234 234 235 235 // Step 2: Listen for page lifecycle events 236 - api.pubsub.subscribe('page:loaded', onPageLoaded, api.scopes.GLOBAL); 237 - api.pubsub.subscribe('page:navigated', onPageNavigated, api.scopes.GLOBAL); 238 - api.pubsub.subscribe('page:will-close', onPageWillClose, api.scopes.GLOBAL); 236 + api.pubsub.subscribe('page:loaded', onPageLoaded); 237 + api.pubsub.subscribe('page:navigated', onPageNavigated); 238 + api.pubsub.subscribe('page:will-close', onPageWillClose); 239 239 240 240 console.log('[ext:pagewidgets-sample] Initialized — listening for page events'); 241 241 };
+3 -3
features/peeks/background.js
··· 155 155 api.pubsub.subscribe('peeks:settings-changed', () => { 156 156 console.log('[ext:peeks] settings changed, reinitializing'); 157 157 reinit(); 158 - }, api.scopes.GLOBAL); 158 + }); 159 159 160 160 // Listen for settings updates from Settings UI 161 161 // Settings UI sends proposed changes, we validate and save ··· 192 192 await reinit(); 193 193 194 194 // Confirm change back to Settings UI 195 - api.pubsub.publish('peeks:settings-changed', currentSettings, api.scopes.GLOBAL); 195 + api.pubsub.publish('peeks:settings-changed', currentSettings); 196 196 } catch (err) { 197 197 console.error('[ext:peeks] settings-update error:', err); 198 198 } 199 - }, api.scopes.GLOBAL); 199 + }); 200 200 }; 201 201 202 202 export default {
+3 -3
features/pubsub-repro/background.js
··· 5 5 * publish `reproduce:response` with a fixed payload on GLOBAL scope. 6 6 * 7 7 * Mirrors websearch background.js pattern: api.pubsub.subscribe/publish 8 - * with api.scopes.GLOBAL via onChannel/emitChannel helpers. 8 + * via onChannel/emitChannel helpers. 9 9 */ 10 10 11 11 const api = window.app; 12 12 13 13 function onChannel(topic, handler) { 14 - api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 14 + api.pubsub.subscribe(topic, handler); 15 15 } 16 16 17 17 function emitChannel(topic, data) { 18 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 18 + api.pubsub.publish(topic, data); 19 19 } 20 20 21 21 const handleRequest = (msg) => {
+2 -2
features/pubsub-repro/home.js
··· 8 8 const api = window.app; 9 9 10 10 function onChannel(topic, handler) { 11 - api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 11 + api.pubsub.subscribe(topic, handler); 12 12 } 13 13 14 14 function emitChannel(topic, data) { 15 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 15 + api.pubsub.publish(topic, data); 16 16 } 17 17 18 18 // Expose for test assertions
+5 -5
features/scripts/background.js
··· 40 40 function onChannel(topic, handler) { 41 41 if (!scriptsChannelHandlers[topic]) scriptsChannelHandlers[topic] = []; 42 42 scriptsChannelHandlers[topic].push(handler); 43 - if (!scriptsChannel) api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 43 + if (!scriptsChannel) api.pubsub.subscribe(topic, handler); 44 44 } 45 45 46 46 function emitChannel(topic, data) { 47 47 if (scriptsChannel) { 48 48 scriptsChannel.postMessage({ topic, data }); 49 49 } else { 50 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 50 + api.pubsub.publish(topic, data); 51 51 } 52 52 } 53 53 ··· 199 199 scriptId, 200 200 result, 201 201 url: executionContext.url 202 - }, api.scopes.GLOBAL); 202 + }); 203 203 204 204 return { success: true, data: result }; 205 205 } catch (error) { ··· 337 337 // Keep scripts:execute on IPC for cross-extension access 338 338 api.pubsub.subscribe('scripts:execute', async (msg) => { 339 339 const result = await executeScript(msg.scriptId, msg.context); 340 - api.pubsub.publish('scripts:execute:response', result, api.scopes.GLOBAL); 341 - }, api.scopes.GLOBAL); 340 + api.pubsub.publish('scripts:execute:response', result); 341 + }); 342 342 343 343 }; 344 344
+2 -2
features/scripts/manager.js
··· 34 34 function onChannel(topic, handler) { 35 35 if (!scriptsChannelHandlers[topic]) scriptsChannelHandlers[topic] = []; 36 36 scriptsChannelHandlers[topic].push(handler); 37 - if (!scriptsChannel) api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 37 + if (!scriptsChannel) api.pubsub.subscribe(topic, handler); 38 38 } 39 39 40 40 function emitChannel(topic, data) { 41 41 if (scriptsChannel) { 42 42 scriptsChannel.postMessage({ topic, data }); 43 43 } else { 44 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 44 + api.pubsub.publish(topic, data); 45 45 } 46 46 } 47 47
+1 -1
features/search/background.js
··· 52 52 api.pubsub.subscribe('cmd:execute:search', (msg) => { 53 53 const query = msg?.data?.query || msg?.data?.text || ''; 54 54 openSearchWindow(query); 55 - }, api.scopes.GLOBAL); 55 + }); 56 56 }; 57 57 58 58 const uninit = () => {
+9 -9
features/search/home.js
··· 292 292 onDelete: async (item) => { 293 293 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 294 294 await api.datastore.deleteItem(item.id); 295 - api.pubsub.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 295 + api.pubsub.publish('item:deleted', { id: item.id }); 296 296 state.results = state.results.filter(r => r.id !== item.id); 297 297 state.itemTags.delete(item.id); 298 298 render(); ··· 300 300 onTagRemove: async (item, tag) => { 301 301 try { 302 302 await api.datastore.untagItem(item.id, tag.id); 303 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 303 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }); 304 304 // Update local tag cache and re-render 305 305 const currentTags = state.itemTags.get(item.id) || []; 306 306 state.itemTags.set(item.id, currentTags.filter(t => t.id !== tag.id)); ··· 330 330 height: 768 331 331 }); 332 332 } else if (itemType === 'text') { 333 - api.pubsub.publish('editor:open', { itemId: item.id }, api.scopes.GLOBAL); 333 + api.pubsub.publish('editor:open', { itemId: item.id }); 334 334 } 335 335 } 336 336 }); ··· 358 358 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 359 359 360 360 await api.datastore.deleteItem(item.id); 361 - api.pubsub.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 361 + api.pubsub.publish('item:deleted', { id: item.id }); 362 362 state.results = state.results.filter(r => r.id !== item.id); 363 363 state.itemTags.delete(item.id); 364 364 render(); ··· 444 444 api.pubsub.subscribe('sync:pull-completed', (msg) => { 445 445 debug && console.log('[search] sync:pull-completed event received:', msg); 446 446 debouncedRefresh(); 447 - }, api.scopes.GLOBAL); 447 + }); 448 448 449 449 api.pubsub.subscribe('item:created', (msg) => { 450 450 debug && console.log('[search] item:created event received:', msg); 451 451 debouncedRefresh(); 452 - }, api.scopes.GLOBAL); 452 + }); 453 453 454 454 api.pubsub.subscribe('item:deleted', (msg) => { 455 455 debug && console.log('[search] item:deleted event received:', msg); 456 456 debouncedRefresh(); 457 - }, api.scopes.GLOBAL); 457 + }); 458 458 459 459 api.pubsub.subscribe('tag:item-added', (msg) => { 460 460 debug && console.log('[search] tag:item-added event received:', msg); 461 461 debouncedRefresh(); 462 - }, api.scopes.GLOBAL); 462 + }); 463 463 464 464 api.pubsub.subscribe('tag:item-removed', (msg) => { 465 465 debug && console.log('[search] tag:item-removed event received:', msg); 466 466 debouncedRefresh(); 467 - }, api.scopes.GLOBAL); 467 + }); 468 468 469 469 document.addEventListener('DOMContentLoaded', init);
+4 -4
features/slides/background.js
··· 238 238 slideWindows.delete(key); 239 239 } 240 240 } 241 - }, api.scopes.GLOBAL); 241 + }); 242 242 243 243 // Initialize slides 244 244 if (currentSettings.items && currentSettings.items.length > 0) { ··· 249 249 api.pubsub.subscribe('slides:settings-changed', () => { 250 250 console.log('[ext:slides] settings changed, reinitializing'); 251 251 reinit(); 252 - }, api.scopes.GLOBAL); 252 + }); 253 253 254 254 // Listen for settings updates from Settings UI 255 255 // Settings UI sends proposed changes, we validate and save ··· 286 286 await reinit(); 287 287 288 288 // Confirm change back to Settings UI 289 - api.pubsub.publish('slides:settings-changed', currentSettings, api.scopes.GLOBAL); 289 + api.pubsub.publish('slides:settings-changed', currentSettings); 290 290 } catch (err) { 291 291 console.error('[ext:slides] settings-update error:', err); 292 292 } 293 - }, api.scopes.GLOBAL); 293 + }); 294 294 }; 295 295 296 296 export default {
+17 -17
features/spaces/background.js
··· 44 44 const exists = await api.window.exists(borderWindowId); 45 45 if (exists?.exists) { 46 46 // Update color and name via message 47 - api.pubsub.publish('spaces:border-update', { color: safeColor, name }, api.scopes.GLOBAL); 47 + api.pubsub.publish('spaces:border-update', { color: safeColor, name }); 48 48 return; 49 49 } 50 50 } catch { /* window gone */ } ··· 132 132 tagId: spaceTagId, 133 133 itemId, 134 134 source: 'spaces-auto-tag' 135 - }, api.scopes.GLOBAL); 135 + }); 136 136 debug && console.log('[ext:spaces] Auto-tagged item', itemId, 'with space', spaceTagId); 137 137 } 138 138 } catch (err) { ··· 336 336 api.pubsub.subscribe('cmd:execute:open space', async (msg) => { 337 337 const result = await openSpace(msg.search?.trim()); 338 338 if (msg.expectResult && msg.resultTopic) { 339 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 339 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 340 340 } 341 - }, api.scopes.GLOBAL); 341 + }); 342 342 api.pubsub.publish('cmd:register', { 343 343 name: 'open space', 344 344 description: 'Open a space (restore saved state or show home)', 345 345 source: 'spaces', 346 346 scope: 'global', 347 347 params: [{ name: 'name', type: 'string', required: true, description: 'Space name' }] 348 - }, api.scopes.GLOBAL); 348 + }); 349 349 350 350 // "close space" 351 351 api.pubsub.subscribe('cmd:execute:close space', async (msg) => { 352 352 const result = await closeSpace(); 353 353 if (msg.expectResult && msg.resultTopic) { 354 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 354 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 355 355 } 356 - }, api.scopes.GLOBAL); 356 + }); 357 357 api.pubsub.publish('cmd:register', { 358 358 name: 'close space', 359 359 description: 'Save window state and close all space windows', 360 360 source: 'spaces', 361 361 scope: 'global', 362 - }, api.scopes.GLOBAL); 362 + }); 363 363 364 364 // "switch space <name>" 365 365 api.pubsub.subscribe('cmd:execute:switch space', async (msg) => { 366 366 const result = await switchSpace(msg.search?.trim()); 367 367 if (msg.expectResult && msg.resultTopic) { 368 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 368 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 369 369 } 370 - }, api.scopes.GLOBAL); 370 + }); 371 371 api.pubsub.publish('cmd:register', { 372 372 name: 'switch space', 373 373 description: 'Close current space and open another', 374 374 source: 'spaces', 375 375 scope: 'global', 376 376 params: [{ name: 'name', type: 'string', required: true, description: 'Target space name' }] 377 - }, api.scopes.GLOBAL); 377 + }); 378 378 379 379 console.log('[ext:spaces] Commands registered: open space, close space, switch space'); 380 380 }; 381 381 382 382 const uninitCommands = () => { 383 383 for (const name of ['open space', 'close space', 'switch space']) { 384 - api.pubsub.publish('cmd:unregister', { name }, api.scopes.GLOBAL); 384 + api.pubsub.publish('cmd:unregister', { name }); 385 385 } 386 386 }; 387 387 ··· 393 393 initCommands(); 394 394 395 395 // Subscribe to item:created for auto-tagging 396 - api.pubsub.subscribe('item:created', handleItemCreated, api.scopes.GLOBAL); 396 + api.pubsub.subscribe('item:created', handleItemCreated); 397 397 398 398 // Subscribe to tag:item-removed for mode reset 399 - api.pubsub.subscribe('tag:item-removed', handleTagRemoved, api.scopes.GLOBAL); 399 + api.pubsub.subscribe('tag:item-removed', handleTagRemoved); 400 400 401 401 // Subscribe to focus events for border visibility 402 - api.pubsub.subscribe('window:focused', () => updateBorderVisibility(), api.scopes.GLOBAL); 403 - api.pubsub.subscribe('window:closed', () => updateBorderVisibility(), api.scopes.GLOBAL); 402 + api.pubsub.subscribe('window:focused', () => updateBorderVisibility()); 403 + api.pubsub.subscribe('window:closed', () => updateBorderVisibility()); 404 404 api.pubsub.subscribe('app:focus-changed', (msg) => { 405 405 if (!msg?.focused) { 406 406 // App lost focus — hide border ··· 408 408 } else { 409 409 updateBorderVisibility(); 410 410 } 411 - }, api.scopes.GLOBAL); 411 + }); 412 412 }; 413 413 414 414 const uninit = () => {
+1 -1
features/spaces/border.html
··· 59 59 if (msg.name) { 60 60 document.getElementById('label').textContent = msg.name; 61 61 } 62 - }, api.scopes.GLOBAL); 62 + }); 63 63 } 64 64 </script> 65 65 </body>
+10 -10
features/spaces/home.js
··· 784 784 if (!confirm(`Delete "${deleteItem.title || deleteItem.content || 'this item'}"?`)) return; 785 785 try { 786 786 await api.datastore.deleteItem(deleteItem.id); 787 - api.pubsub.publish('item:deleted', { id: deleteItem.id }, api.scopes.GLOBAL); 787 + api.pubsub.publish('item:deleted', { id: deleteItem.id }); 788 788 if (state.currentSpace) { await loadItemsForSpace(state.currentSpace.id); renderItems(); } 789 789 } catch (err) { 790 790 console.error('[spaces] Failed to delete item:', err); ··· 833 833 e.stopPropagation(); 834 834 try { 835 835 await api.datastore.untagItem(item.id, state.currentSpace.id); 836 - api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: state.currentSpace.id }, api.scopes.GLOBAL); 836 + api.pubsub.publish('tag:item-removed', { itemId: item.id, tagId: state.currentSpace.id }); 837 837 if (state.currentSpace) { await loadItemsForSpace(state.currentSpace.id); renderItems(); } 838 838 } catch (err) { 839 839 console.error('[spaces] Failed to remove item from space:', err); ··· 880 880 } 881 881 }, 150); 882 882 883 - api.pubsub.subscribe('tag:item-added', () => debouncedRefresh(), api.scopes.GLOBAL); 884 - api.pubsub.subscribe('tag:item-removed', () => debouncedRefresh(), api.scopes.GLOBAL); 885 - api.pubsub.subscribe('tag:created', () => debouncedRefresh(), api.scopes.GLOBAL); 886 - api.pubsub.subscribe('item:created', () => debouncedRefresh(), api.scopes.GLOBAL); 887 - api.pubsub.subscribe('item:deleted', () => debouncedRefresh(), api.scopes.GLOBAL); 888 - api.pubsub.subscribe('sync:pull-completed', () => debouncedRefresh(), api.scopes.GLOBAL); 889 - api.pubsub.subscribe('tag:renamed', () => debouncedRefresh(), api.scopes.GLOBAL); 890 - api.pubsub.subscribe('tag:color-changed', () => debouncedRefresh(), api.scopes.GLOBAL); 883 + api.pubsub.subscribe('tag:item-added', () => debouncedRefresh()); 884 + api.pubsub.subscribe('tag:item-removed', () => debouncedRefresh()); 885 + api.pubsub.subscribe('tag:created', () => debouncedRefresh()); 886 + api.pubsub.subscribe('item:created', () => debouncedRefresh()); 887 + api.pubsub.subscribe('item:deleted', () => debouncedRefresh()); 888 + api.pubsub.subscribe('sync:pull-completed', () => debouncedRefresh()); 889 + api.pubsub.subscribe('tag:renamed', () => debouncedRefresh()); 890 + api.pubsub.subscribe('tag:color-changed', () => debouncedRefresh()); 891 891 892 892 setupCreateSpace(); 893 893
+3 -3
features/sync/background.html
··· 29 29 labels: extension.labels, 30 30 version: '1.0.0' 31 31 } 32 - }, api.scopes.SYSTEM); 32 + }); 33 33 34 34 // Handle shutdown request from main process 35 35 api.pubsub.subscribe('app:shutdown', () => { ··· 37 37 if (extension.uninit) { 38 38 extension.uninit(); 39 39 } 40 - }, api.scopes.SYSTEM); 40 + }); 41 41 42 42 // Handle extension-specific shutdown 43 43 api.pubsub.subscribe(`ext:${extId}:shutdown`, () => { ··· 45 45 if (extension.uninit) { 46 46 extension.uninit(); 47 47 } 48 - }, api.scopes.SYSTEM); 48 + }); 49 49 </script> 50 50 </body> 51 51 </html>
+12 -12
features/tag-actions/background.js
··· 204 204 api.pubsub.publish('tag-actions:get-pairs:response', { 205 205 success: true, 206 206 data: currentPairs 207 - }, api.scopes.GLOBAL); 208 - }, api.scopes.GLOBAL); 207 + }); 208 + }); 209 209 210 210 api.pubsub.subscribe('tag-actions:set-pairs', async (msg) => { 211 211 if (Array.isArray(msg.pairs)) { ··· 215 215 api.pubsub.publish('tag-actions:set-pairs:response', { 216 216 success: true, 217 217 data: currentPairs 218 - }, api.scopes.GLOBAL); 219 - }, api.scopes.GLOBAL); 218 + }); 219 + }); 220 220 221 221 // Backward-compatible: respond to get-all with derived action rules 222 222 // so tag-action-affordances.js cache still works ··· 224 224 api.pubsub.publish('tag-actions:get-all:response', { 225 225 success: true, 226 226 data: currentActions 227 - }, api.scopes.GLOBAL); 228 - }, api.scopes.GLOBAL); 227 + }); 228 + }); 229 229 230 230 // Legacy CRUD endpoints -- respond gracefully but pairs is the source of truth now 231 231 api.pubsub.subscribe('tag-actions:create', async (msg) => { ··· 238 238 }); 239 239 await saveSettings(); 240 240 } 241 - api.pubsub.publish('tag-actions:create:response', { success: true }, api.scopes.GLOBAL); 242 - }, api.scopes.GLOBAL); 241 + api.pubsub.publish('tag-actions:create:response', { success: true }); 242 + }); 243 243 244 244 api.pubsub.subscribe('tag-actions:update', async (msg) => { 245 - api.pubsub.publish('tag-actions:update:response', { success: true }, api.scopes.GLOBAL); 246 - }, api.scopes.GLOBAL); 245 + api.pubsub.publish('tag-actions:update:response', { success: true }); 246 + }); 247 247 248 248 api.pubsub.subscribe('tag-actions:delete', async (msg) => { 249 - api.pubsub.publish('tag-actions:delete:response', { success: true }, api.scopes.GLOBAL); 250 - }, api.scopes.GLOBAL); 249 + api.pubsub.publish('tag-actions:delete:response', { success: true }); 250 + }); 251 251 252 252 debug && console.log('[tag-actions] initialized with', currentPairs.length, 'pairs'); 253 253 };
+8 -8
features/tag-actions/home.js
··· 231 231 api.pubsub.publish('tag-actions:get-all:response', { 232 232 success: true, 233 233 data: currentActions 234 - }, api.scopes.GLOBAL); 235 - }, api.scopes.GLOBAL); 234 + }); 235 + }); 236 236 237 237 // Legacy CRUD endpoints -- respond gracefully; pairs is the source of truth 238 238 api.pubsub.subscribe('tag-actions:create', async (msg) => { ··· 245 245 }); 246 246 await saveSettings(); 247 247 } 248 - api.pubsub.publish('tag-actions:create:response', { success: true }, api.scopes.GLOBAL); 249 - }, api.scopes.GLOBAL); 248 + api.pubsub.publish('tag-actions:create:response', { success: true }); 249 + }); 250 250 251 251 api.pubsub.subscribe('tag-actions:update', async (_msg) => { 252 - api.pubsub.publish('tag-actions:update:response', { success: true }, api.scopes.GLOBAL); 253 - }, api.scopes.GLOBAL); 252 + api.pubsub.publish('tag-actions:update:response', { success: true }); 253 + }); 254 254 255 255 api.pubsub.subscribe('tag-actions:delete', async (_msg) => { 256 - api.pubsub.publish('tag-actions:delete:response', { success: true }, api.scopes.GLOBAL); 257 - }, api.scopes.GLOBAL); 256 + api.pubsub.publish('tag-actions:delete:response', { success: true }); 257 + }); 258 258 }; 259 259 260 260 // ==================== Escape Handler ====================
+2 -2
features/tags/background.js
··· 343 343 if (ctx.search) { 344 344 try { 345 345 const { id, tags } = await createTagset(ctx.search); 346 - api.pubsub.publish('editor:changed', { action: 'add', itemId: id }, api.scopes.GLOBAL); 346 + api.pubsub.publish('editor:changed', { action: 'add', itemId: id }); 347 347 return { success: true, message: `Tagset created with tags: ${tags.join(', ')}` }; 348 348 } catch (error) { 349 349 console.error('Failed to create tagset:', error); 350 350 return { success: false, message: error.message }; 351 351 } 352 352 } else { 353 - api.pubsub.publish('editor:add', { type: 'tagset' }, api.scopes.GLOBAL); 353 + api.pubsub.publish('editor:add', { type: 'tagset' }); 354 354 return { success: true, message: 'Opening editor' }; 355 355 } 356 356 }
+13 -13
features/tags/home.js
··· 468 468 state.itemTags.set(msg.itemId, currentTags); 469 469 } 470 470 debouncedRefresh(); 471 - }, api.scopes.GLOBAL); 471 + }); 472 472 473 473 api.pubsub.subscribe('tag:item-removed', (msg) => { 474 474 debug && console.log('[tags] tag:item-removed event received:', msg); ··· 476 476 const currentTags = state.itemTags.get(msg.itemId) || []; 477 477 state.itemTags.set(msg.itemId, currentTags.filter(t => t.id !== msg.tagId)); 478 478 debouncedRefresh(); 479 - }, api.scopes.GLOBAL); 479 + }); 480 480 481 481 api.pubsub.subscribe('tag:created', (msg) => { 482 482 debug && console.log('[tags] tag:created event received:', msg); ··· 485 485 state.tags.push({ id: msg.tagId, name: msg.tagName }); 486 486 } 487 487 debouncedRefresh(); 488 - }, api.scopes.GLOBAL); 488 + }); 489 489 490 490 api.pubsub.subscribe('tag:renamed', (msg) => { 491 491 debug && console.log('[tags] tag:renamed event received:', msg); 492 492 debouncedRefresh(); 493 - }, api.scopes.GLOBAL); 493 + }); 494 494 495 495 api.pubsub.subscribe('tag:deleted', (msg) => { 496 496 debug && console.log('[tags] tag:deleted event received:', msg); 497 497 debouncedRefresh(); 498 - }, api.scopes.GLOBAL); 498 + }); 499 499 500 500 // Subscribe to item events for reactive updates 501 501 api.pubsub.subscribe('item:created', (msg) => { 502 502 debug && console.log('[tags] item:created event received:', msg); 503 503 debouncedRefresh(); 504 - }, api.scopes.GLOBAL); 504 + }); 505 505 506 506 api.pubsub.subscribe('item:deleted', (msg) => { 507 507 debug && console.log('[tags] item:deleted event received:', msg); ··· 509 509 state.items = state.items.filter(i => i.id !== msg.itemId); 510 510 state.itemTags.delete(msg.itemId); 511 511 debouncedRefresh(); 512 - }, api.scopes.GLOBAL); 512 + }); 513 513 514 514 api.pubsub.subscribe('item:updated', (msg) => { 515 515 debug && console.log('[tags] item:updated event received:', msg); 516 516 debouncedRefresh(); 517 - }, api.scopes.GLOBAL); 517 + }); 518 518 519 519 // Subscribe to sync events — sync operations bypass per-item events, 520 520 // so we need this to refresh when new data arrives via sync 521 521 api.pubsub.subscribe('sync:pull-completed', (msg) => { 522 522 debug && console.log('[tags] sync:pull-completed event received:', msg); 523 523 debouncedRefresh(); 524 - }, api.scopes.GLOBAL); 524 + }); 525 525 526 526 // Subscribe to entity extraction events to update entity filter bar 527 527 api.pubsub.subscribe('entities:extracted', (msg) => { 528 528 debug && console.log('[tags] entities:extracted event received:', msg); 529 529 debouncedRefresh(); 530 - }, api.scopes.GLOBAL); 530 + }); 531 531 532 532 // Search input 533 533 searchInput.addEventListener('input', (e) => { ··· 578 578 579 579 // Add item button 580 580 document.querySelector('.add-item-btn').addEventListener('click', () => { 581 - api.pubsub.publish('editor:add', {}, api.scopes.GLOBAL); 581 + api.pubsub.publish('editor:add', {}); 582 582 }); 583 583 584 584 // Keyboard navigation ··· 613 613 } 614 614 debug && console.log('[tags] Updated embedded editor vimMode:', editorVimMode); 615 615 } 616 - }, api.scopes.GLOBAL); 616 + }); 617 617 } 618 618 }; 619 619 ··· 1300 1300 1301 1301 // Publish change event for other extensions 1302 1302 if (api?.pubsub?.publish) { 1303 - api.pubsub.publish('editor:changed', { action: 'update', itemId: state.editingItem.id }, api.scopes.GLOBAL); 1303 + api.pubsub.publish('editor:changed', { action: 'update', itemId: state.editingItem.id }); 1304 1304 } 1305 1305 } else { 1306 1306 console.error('[tags] Editor autosave failed:', result.error);
+28 -28
features/timers/background.js
··· 332 332 id: timer.id, 333 333 title: timer.title, 334 334 timerType: timer.metadata.timerType 335 - }, api.scopes.GLOBAL); 335 + }); 336 336 337 337 debug && console.log('[ext:timers] Timer completed:', timer.id); 338 338 }; ··· 358 358 id: timer.id, 359 359 title: timer.title, 360 360 count: timer.metadata.intervalCount 361 - }, api.scopes.GLOBAL); 361 + }); 362 362 }; 363 363 364 364 // ===== Tick Loop ===== ··· 435 435 436 436 // Publish tick for UI updates 437 437 if (tickData.length > 0) { 438 - api.pubsub.publish('timer:tick', { timers: tickData }, api.scopes.GLOBAL); 438 + api.pubsub.publish('timer:tick', { timers: tickData }); 439 439 } 440 440 441 441 // Update HUD with active timer summary ··· 473 473 const updateHud = (tickData) => { 474 474 const running = tickData.filter(t => t.status === 'running'); 475 475 if (running.length === 0) { 476 - api.pubsub.publish('timer:hud-update', { text: '' }, api.scopes.GLOBAL); 476 + api.pubsub.publish('timer:hud-update', { text: '' }); 477 477 return; 478 478 } 479 479 ··· 482 482 const text = count === 1 483 483 ? `${running[0].display}` 484 484 : `${count} timers: ${displays.join(', ')}`; 485 - api.pubsub.publish('timer:hud-update', { text, count }, api.scopes.GLOBAL); 485 + api.pubsub.publish('timer:hud-update', { text, count }); 486 486 }; 487 487 488 488 // ===== Restore Running Timers ===== ··· 726 726 } 727 727 728 728 await updateTimerMetadata(timer.id, timer.metadata); 729 - api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'paused' }, api.scopes.GLOBAL); 729 + api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'paused' }); 730 730 debug && console.log('[ext:timers] Paused timer:', timer.id); 731 - }, api.scopes.GLOBAL); 731 + }); 732 732 733 733 // Resume a timer 734 734 api.pubsub.subscribe('timer:resume', async (msg) => { ··· 757 757 timer.metadata.pausedAt = null; 758 758 await updateTimerMetadata(timer.id, timer.metadata); 759 759 startTickLoop(); 760 - api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'running' }, api.scopes.GLOBAL); 760 + api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'running' }); 761 761 debug && console.log('[ext:timers] Resumed timer:', timer.id); 762 - }, api.scopes.GLOBAL); 762 + }); 763 763 764 764 // Stop a timer 765 765 api.pubsub.subscribe('timer:stop', async (msg) => { ··· 776 776 await updateTimerMetadata(timer.id, timer.metadata); 777 777 activeTimers.delete(timer.id); 778 778 stopTickLoopIfEmpty(); 779 - api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'stopped' }, api.scopes.GLOBAL); 779 + api.pubsub.publish('timer:state-changed', { id: timer.id, status: 'stopped' }); 780 780 debug && console.log('[ext:timers] Stopped timer:', timer.id); 781 - }, api.scopes.GLOBAL); 781 + }); 782 782 783 783 // Delete a timer 784 784 api.pubsub.subscribe('timer:delete', async (msg) => { ··· 789 789 } catch (err) { 790 790 console.error('[ext:timers] Failed to delete timer:', err); 791 791 } 792 - api.pubsub.publish('timer:state-changed', { id: msg.id, status: 'deleted' }, api.scopes.GLOBAL); 792 + api.pubsub.publish('timer:state-changed', { id: msg.id, status: 'deleted' }); 793 793 debug && console.log('[ext:timers] Deleted timer:', msg.id); 794 - }, api.scopes.GLOBAL); 794 + }); 795 795 796 796 // Request current timer state (UI requests on load) 797 797 api.pubsub.subscribe('timer:request-state', async () => { ··· 816 816 } catch (err) { 817 817 console.error('[ext:timers] Failed to query timers:', err); 818 818 } 819 - api.pubsub.publish('timer:state-response', { timers }, api.scopes.GLOBAL); 820 - }, api.scopes.GLOBAL); 819 + api.pubsub.publish('timer:state-response', { timers }); 820 + }); 821 821 }; 822 822 823 823 // ===== Registration ===== ··· 836 836 api.pubsub.subscribe('cmd:execute:timer countdown', async (msg) => { 837 837 const result = await handleCountdown(msg.search?.trim()); 838 838 if (msg.expectResult && msg.resultTopic) { 839 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 839 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 840 840 } 841 - }, api.scopes.GLOBAL); 841 + }); 842 842 api.pubsub.publish('cmd:register', { 843 843 name: 'timer countdown', 844 844 description: 'Start a countdown timer (e.g., 30m, 1h, 5m30s)', ··· 847 847 accepts: [], 848 848 produces: [], 849 849 params: [{ name: 'duration', type: 'string', required: true, description: 'Duration (e.g., 30m, 1h, 5m30s, 90s)' }] 850 - }, api.scopes.GLOBAL); 850 + }); 851 851 852 852 api.pubsub.subscribe('cmd:execute:timer alarm', async (msg) => { 853 853 const result = await handleAlarm(msg.search?.trim()); 854 854 if (msg.expectResult && msg.resultTopic) { 855 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 855 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 856 856 } 857 - }, api.scopes.GLOBAL); 857 + }); 858 858 api.pubsub.publish('cmd:register', { 859 859 name: 'timer alarm', 860 860 description: 'Set an alarm for a specific time (e.g., 6pm, 14:30)', ··· 863 863 accepts: [], 864 864 produces: [], 865 865 params: [{ name: 'time', type: 'string', required: true, description: 'Time (e.g., 6pm, 14:30, 6:30pm)' }] 866 - }, api.scopes.GLOBAL); 866 + }); 867 867 868 868 api.pubsub.subscribe('cmd:execute:timer stopwatch', async (msg) => { 869 869 const result = await handleStopwatch(msg.search?.trim()); 870 870 if (msg.expectResult && msg.resultTopic) { 871 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 871 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 872 872 } 873 - }, api.scopes.GLOBAL); 873 + }); 874 874 api.pubsub.publish('cmd:register', { 875 875 name: 'timer stopwatch', 876 876 description: 'Start a stopwatch that counts up', ··· 879 879 accepts: [], 880 880 produces: [], 881 881 params: [{ name: 'name', type: 'string', required: false, description: 'Optional name for the stopwatch' }] 882 - }, api.scopes.GLOBAL); 882 + }); 883 883 884 884 api.pubsub.subscribe('cmd:execute:timer interval', async (msg) => { 885 885 const result = await handleInterval(msg.search?.trim()); 886 886 if (msg.expectResult && msg.resultTopic) { 887 - api.pubsub.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 887 + api.pubsub.publish(msg.resultTopic, result || { success: true }); 888 888 } 889 - }, api.scopes.GLOBAL); 889 + }); 890 890 api.pubsub.publish('cmd:register', { 891 891 name: 'timer interval', 892 892 description: 'Start a repeating interval timer', ··· 895 895 accepts: [], 896 896 produces: [], 897 897 params: [{ name: 'duration', type: 'string', required: true, description: 'Interval duration (e.g., 25m, 1h)' }] 898 - }, api.scopes.GLOBAL); 898 + }); 899 899 900 900 // Set up pubsub handlers for timer control 901 901 setupPubsubHandlers(); ··· 912 912 } 913 913 // Unregister standalone commands 914 914 for (const name of ['timer countdown', 'timer alarm', 'timer stopwatch', 'timer interval']) { 915 - api.pubsub.publish('cmd:unregister', { name }, api.scopes.GLOBAL); 915 + api.pubsub.publish('cmd:unregister', { name }); 916 916 } 917 917 }; 918 918
+11 -11
features/timers/home.js
··· 183 183 pauseBtn.addEventListener('click', (e) => { 184 184 e.stopPropagation(); 185 185 if (meta.status === 'paused') { 186 - api.pubsub.publish('timer:resume', { id: timer.id }, api.scopes.GLOBAL); 186 + api.pubsub.publish('timer:resume', { id: timer.id }); 187 187 } else { 188 - api.pubsub.publish('timer:pause', { id: timer.id }, api.scopes.GLOBAL); 188 + api.pubsub.publish('timer:pause', { id: timer.id }); 189 189 } 190 190 }); 191 191 btnContainer.appendChild(pauseBtn); ··· 198 198 stopBtn.textContent = 'Stop'; 199 199 stopBtn.addEventListener('click', (e) => { 200 200 e.stopPropagation(); 201 - api.pubsub.publish('timer:stop', { id: timer.id }, api.scopes.GLOBAL); 201 + api.pubsub.publish('timer:stop', { id: timer.id }); 202 202 }); 203 203 btnContainer.appendChild(stopBtn); 204 204 ··· 215 215 deleteBtn.textContent = 'Delete'; 216 216 deleteBtn.addEventListener('click', (e) => { 217 217 e.stopPropagation(); 218 - api.pubsub.publish('timer:delete', { id: timer.id }, api.scopes.GLOBAL); 218 + api.pubsub.publish('timer:delete', { id: timer.id }); 219 219 }); 220 220 footer.appendChild(deleteBtn); 221 221 } ··· 325 325 allTimers = msg.timers; 326 326 render(); 327 327 } 328 - }, api.scopes.GLOBAL); 328 + }); 329 329 330 330 // Subscribe to tick events for live display updates 331 - api.pubsub.subscribe('timer:tick', handleTick, api.scopes.GLOBAL); 331 + api.pubsub.subscribe('timer:tick', handleTick); 332 332 333 333 // Subscribe to state changes (pause, resume, stop, delete, complete) 334 334 api.pubsub.subscribe('timer:state-changed', () => { 335 335 // Re-request full state to re-render 336 - api.pubsub.publish('timer:request-state', {}, api.scopes.GLOBAL); 337 - }, api.scopes.GLOBAL); 336 + api.pubsub.publish('timer:request-state', {}); 337 + }); 338 338 339 339 api.pubsub.subscribe('timer:completed', () => { 340 - api.pubsub.publish('timer:request-state', {}, api.scopes.GLOBAL); 341 - }, api.scopes.GLOBAL); 340 + api.pubsub.publish('timer:request-state', {}); 341 + }); 342 342 343 343 // Request initial state 344 - api.pubsub.publish('timer:request-state', {}, api.scopes.GLOBAL); 344 + api.pubsub.publish('timer:request-state', {}); 345 345 }; 346 346 347 347 document.addEventListener('DOMContentLoaded', init);
+9 -9
features/websearch/background.js
··· 25 25 // — different origins, so BroadcastChannel cannot work. Use IPC pubsub instead. 26 26 27 27 function onChannel(topic, handler) { 28 - api.pubsub.subscribe(topic, handler, api.scopes.GLOBAL); 28 + api.pubsub.subscribe(topic, handler); 29 29 } 30 30 31 31 function emitChannel(topic, data) { 32 - api.pubsub.publish(topic, data, api.scopes.GLOBAL); 32 + api.pubsub.publish(topic, data); 33 33 } 34 34 35 35 const address = 'peek://websearch/home.html'; ··· 415 415 // Publish discovery event for UI notifications 416 416 api.pubsub.publish('websearch:engine-discovered', { 417 417 engine: { id: engine.id, name: engine.name, searchUrl: engine.searchUrl } 418 - }, api.scopes.GLOBAL); 418 + }); 419 419 420 420 debug && console.log('[ext:websearch] Discovered search engine:', engine.name, 'from', pageUrl); 421 421 return engine; ··· 713 713 if (msg.engineId && getEngine(msg.engineId)) { 714 714 currentSettings.prefs.defaultEngine = msg.engineId; 715 715 await saveSettings(currentSettings); 716 - api.pubsub.publish('websearch:settings-changed', currentSettings, api.scopes.SELF); 716 + api.pubsub.publish('websearch:settings-changed', currentSettings); 717 717 } 718 718 break; 719 719 ··· 745 745 initCommands(); 746 746 747 747 // Listen for page load events for OpenSearch discovery 748 - api.pubsub.subscribe('page:loaded', handlePageLoaded, api.scopes.GLOBAL); 748 + api.pubsub.subscribe('page:loaded', handlePageLoaded); 749 749 750 750 // Listen for suggestion requests via BroadcastChannel 751 751 onChannel('websearch:request-suggestions', handleSuggestionRequest); ··· 760 760 debug && console.log('[ext:websearch] Search window closed'); 761 761 searchWindowId = null; 762 762 } 763 - }, api.scopes.GLOBAL); 763 + }); 764 764 765 765 // Listen for settings changes to hot-reload 766 766 api.pubsub.subscribe('websearch:settings-changed', async () => { ··· 769 769 currentSettings = await loadSettings(); 770 770 await loadEngines(); 771 771 initCommands(); 772 - }, api.scopes.SELF); 772 + }); 773 773 774 774 // Listen for settings updates from Settings UI 775 775 api.pubsub.subscribe('websearch:settings-update', async (msg) => { ··· 798 798 await loadEngines(); 799 799 initCommands(); 800 800 801 - api.pubsub.publish('websearch:settings-changed', currentSettings, api.scopes.SELF); 801 + api.pubsub.publish('websearch:settings-changed', currentSettings); 802 802 } catch (err) { 803 803 console.error('[ext:websearch] settings-update error:', err); 804 804 } 805 - }, api.scopes.GLOBAL); 805 + }); 806 806 }; 807 807 808 808 const uninit = () => {
+6 -6
features/websearch/home.js
··· 396 396 // Publish discovery event for UI notifications 397 397 api.pubsub.publish('websearch:engine-discovered', { 398 398 engine: { id: engine.id, name: engine.name, searchUrl: engine.searchUrl } 399 - }, api.scopes.GLOBAL); 399 + }); 400 400 401 401 debug && console.log('[ext:websearch] Discovered search engine:', engine.name, 'from', pageUrl); 402 402 return engine; ··· 1040 1040 syncEnginesToUi(); 1041 1041 updateEngineIndicator(); 1042 1042 render(); 1043 - }, api.scopes.GLOBAL); 1043 + }); 1044 1044 1045 1045 // Subscribe to settings changes to refresh UI 1046 1046 api.pubsub.subscribe('websearch:settings-changed', () => { 1047 1047 syncEnginesToUi(); 1048 1048 updateEngineIndicator(); 1049 1049 render(); 1050 - }, api.scopes.SELF); 1050 + }); 1051 1051 1052 1052 // Set up search input 1053 1053 const searchInput = document.getElementById('search-input'); ··· 1115 1115 initCommands(); 1116 1116 1117 1117 // Listen for page load events for OpenSearch discovery 1118 - api.pubsub.subscribe('page:loaded', handlePageLoaded, api.scopes.GLOBAL); 1118 + api.pubsub.subscribe('page:loaded', handlePageLoaded); 1119 1119 1120 1120 // Listen for settings updates from Settings UI 1121 1121 api.pubsub.subscribe('websearch:settings-update', async (msg) => { ··· 1144 1144 await loadEngines(); 1145 1145 initCommands(); 1146 1146 1147 - api.pubsub.publish('websearch:settings-changed', currentSettings, api.scopes.SELF); 1147 + api.pubsub.publish('websearch:settings-changed', currentSettings); 1148 1148 } catch (err) { 1149 1149 console.error('[ext:websearch] settings-update error:', err); 1150 1150 } 1151 - }, api.scopes.GLOBAL); 1151 + }); 1152 1152 1153 1153 // Register shutdown handler 1154 1154 api.onShutdown(() => {
+1 -1
features/windows/background.js
··· 173 173 debug && console.log('[ext:windows] Windows view closed'); 174 174 windowsViewId = null; 175 175 } 176 - }, api.scopes.GLOBAL); 176 + }); 177 177 }; 178 178 179 179 const uninit = () => {