experiments in a post-browser web
10
fork

Configure Feed

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

refactor(core): consolidate page resident into core background renderer

Moves page's `open` and `modal` command registration from a standalone
page-resident BrowserWindow created by page-glue.ts into the core
background renderer. app/page/background.js is a new module exporting
initPage/uninitPage; app/index.js awaits initPage() right after
initCmd(). app/page/background.html and backend/electron/page-glue.ts
are deleted.

The fullscreen-transparent canvas page windows (one per URL) are
unchanged — they're still created on demand via api.window.open(...)
from the command handlers. The canvas architecture in
app/page/index.html + page.js is untouched.

core.spec.ts 7/7 and external-url.spec.ts 8/8 pass, confirming the
`open` command still routes correctly.

+150 -367
+2
app/index.js
··· 6 6 import migrations from './migrations/index.js'; 7 7 import { log } from './log.js'; 8 8 import { initCmd } from './cmd/background.js'; 9 + import { initPage } from './page/background.js'; 9 10 10 11 const { id, labels, schemas, storageKeys, defaults } = appConfig; 11 12 ··· 598 599 // cmd-resident BrowserWindow created by cmd-glue.ts; now it runs inside 599 600 // this core background renderer. 600 601 await initCmd(); 602 + await initPage(); 601 603 602 604 // Register ext:all-loaded subscriber BEFORE publishing topicCorePrefs. 603 605 // Main subscribes to topicCorePrefs and kicks off loadExtensions() which
-209
app/page/background.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="utf-8"> 5 - <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 6 - <title>Page (resident)</title> 7 - </head> 8 - <body> 9 - <!-- 10 - Page resident renderer. 11 - 12 - This is the single-file core renderer for the page subsystem. It 13 - replaces the v1 pair of `background.html` + `background.js` (iframe 14 - inside the consolidated extension host) with a standalone, hidden, 15 - resident BrowserWindow loaded by `backend/electron/page-glue.ts` with 16 - `tile-preload.cjs` and a trusted-builtin capability grant. 17 - 18 - The window has no visible UI — the page canvas itself (the fullscreen 19 - transparent BrowserWindow that hosts the webview) is created on demand 20 - per URL at `peek://app/page/index.html?url=...`. That canvas 21 - architecture is FROZEN (see DEVELOPMENT.md) and is unchanged. 22 - 23 - This resident renderer exists solely to register the `open` and `modal` 24 - commands against the cmd registry via the portable 25 - `api.commands.register()` surface. 26 - --> 27 - <script type="module"> 28 - const api = window.app; 29 - 30 - const id = 'page'; 31 - const labels = { 32 - name: 'Page', 33 - description: 'Page and window commands' 34 - }; 35 - 36 - // ── V2 Tile Runtime ── 37 - // Initialize the tile preload surface before any handler registration. 38 - // With the trustedBuiltin grant every IPC call is un-gated, but the 39 - // preload still needs its async token validation to complete before 40 - // `api.pubsub.*` / `api.commands.*` can dispatch. 41 - if (api.initialize) { 42 - await api.initialize(); 43 - } 44 - 45 - // ===== URL Validation ===== 46 - 47 - /** 48 - * Validates and normalizes a URL string 49 - * @param {string} str - The string to check 50 - * @returns {Object} - Object with valid flag and normalized URL 51 - */ 52 - function getValidURL(str) { 53 - // Quick check for empty string 54 - if (!str) return { valid: false }; 55 - 56 - // Check if it starts with a valid protocol 57 - const hasValidProtocol = /^(https?|ftp|file|peek):\/\//.test(str); 58 - 59 - if (!hasValidProtocol) { 60 - // Check if it looks like a domain (e.g., "example.com", "example.com/path", "localhost") 61 - // Pattern: domain.tld or domain.tld/path (with optional port for localhost) 62 - const isDomainPattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/.test(str); 63 - const isLocalhost = /^localhost(:\d+)?(\/.*)?$/.test(str); 64 - 65 - if (isDomainPattern || isLocalhost) { 66 - // It's a domain without protocol, add https:// 67 - const urlWithProtocol = 'https://' + str; 68 - try { 69 - // Validate the URL with added protocol 70 - new URL(urlWithProtocol); 71 - return { valid: true, url: urlWithProtocol }; 72 - } catch (e) { 73 - return { valid: false }; 74 - } 75 - } 76 - return { valid: false }; 77 - } 78 - 79 - try { 80 - // Already has protocol, just validate 81 - new URL(str); 82 - return { valid: true, url: str }; 83 - } catch (e) { 84 - return { valid: false }; 85 - } 86 - } 87 - 88 - // ===== Command definitions ===== 89 - 90 - const commandDefinitions = [ 91 - { 92 - name: 'open', 93 - description: 'Open a URL in a new window', 94 - execute: async (ctx) => { 95 - const parts = ctx.typed.split(' '); 96 - parts.shift(); 97 - 98 - const address = parts.shift(); 99 - 100 - if (!address) { 101 - return { error: 'No address provided' }; 102 - } 103 - 104 - // Check if the input is a valid URL and get the normalized version 105 - const urlResult = getValidURL(address); 106 - if (!urlResult.valid) { 107 - return { error: 'Invalid URL. Must be a valid URL starting with http://, https://, or other valid protocol.' }; 108 - } 109 - 110 - // Use the normalized URL (with protocol added if needed) 111 - const normalizedAddress = urlResult.url; 112 - 113 - try { 114 - await api.window.open(normalizedAddress, { 115 - width: 1024, 116 - height: 768, 117 - trackingSource: 'cmd', 118 - trackingSourceId: 'open' 119 - }); 120 - 121 - return { 122 - command: 'open', 123 - address: normalizedAddress, 124 - success: true 125 - }; 126 - } catch (error) { 127 - console.error('[page] Failed to open window:', error); 128 - return { 129 - error: 'Failed to open window: ' + error.message, 130 - address: normalizedAddress 131 - }; 132 - } 133 - } 134 - }, 135 - { 136 - name: 'modal', 137 - description: 'Open a URL in a modal window that hides on blur or escape', 138 - execute: async (ctx) => { 139 - const parts = ctx.typed.split(' '); 140 - parts.shift(); 141 - 142 - const address = parts.shift(); 143 - 144 - if (!address) { 145 - return { error: 'No address provided' }; 146 - } 147 - 148 - try { 149 - await api.window.openModal(address, { 150 - width: 700, 151 - height: 500 152 - }); 153 - } catch (error) { 154 - console.error('[page] Failed to open modal window:', error); 155 - return { error: 'Failed to open modal window: ' + error.message }; 156 - } 157 - 158 - return { 159 - command: 'modal', 160 - address 161 - }; 162 - } 163 - } 164 - ]; 165 - 166 - // ===== Registration ===== 167 - 168 - const registeredCommands = []; 169 - 170 - for (const cmd of commandDefinitions) { 171 - api.commands.register(cmd); 172 - registeredCommands.push(cmd.name); 173 - } 174 - 175 - // Collect registered command topics for assertion verification 176 - const registeredTopics = registeredCommands.map(name => `cmd:execute:${name}`); 177 - 178 - // Signal ready to main process — only after commands are registered. 179 - // The v2 surface routes api.pubsub.publish through tile:pubsub:publish, 180 - // which in turn feeds the same main-process `publish()` that the v1 181 - // iframe path fed. 182 - api.pubsub.publish('ext:ready', { 183 - id, 184 - registeredTopics, 185 - manifest: { 186 - id, 187 - labels, 188 - version: '1.0.0' 189 - } 190 - }, api.scopes.SYSTEM); 191 - 192 - // Handle shutdown request from main process 193 - api.pubsub.subscribe('app:shutdown', () => { 194 - console.log(`[page] received shutdown`); 195 - for (const name of registeredCommands) { 196 - api.commands.unregister(name); 197 - } 198 - }, api.scopes.SYSTEM); 199 - 200 - // Handle extension-specific shutdown 201 - api.pubsub.subscribe(`ext:${id}:shutdown`, () => { 202 - console.log(`[page] received extension shutdown`); 203 - for (const name of registeredCommands) { 204 - api.commands.unregister(name); 205 - } 206 - }, api.scopes.SYSTEM); 207 - </script> 208 - </body> 209 - </html>
+139
app/page/background.js
··· 1 + /** 2 + * Page — `open` and `modal` command registration. 3 + * 4 + * Runs INSIDE the core background renderer (app/index.js imports this 5 + * module and awaits initPage()). Previously this ran in a separate 6 + * `peek://page/background.html` resident BrowserWindow created by 7 + * page-glue.ts; that window is gone — the logic moved into the core 8 + * renderer alongside cmd. 9 + * 10 + * The fullscreen-transparent canvas page windows (one per URL) are 11 + * created on demand by `api.window.open(...)` from the `open` / `modal` 12 + * command handlers below. That canvas architecture (in 13 + * app/page/index.html + app/page/page.js) is FROZEN — unchanged by 14 + * this consolidation. 15 + */ 16 + 17 + const api = window.app; 18 + 19 + /** 20 + * Validate and normalize a URL string. 21 + */ 22 + function getValidURL(str) { 23 + if (!str) return { valid: false }; 24 + 25 + const hasValidProtocol = /^(https?|ftp|file|peek):\/\//.test(str); 26 + 27 + if (!hasValidProtocol) { 28 + const isDomainPattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(\/.*)?$/.test(str); 29 + const isLocalhost = /^localhost(:\d+)?(\/.*)?$/.test(str); 30 + 31 + if (isDomainPattern || isLocalhost) { 32 + const urlWithProtocol = 'https://' + str; 33 + try { 34 + new URL(urlWithProtocol); 35 + return { valid: true, url: urlWithProtocol }; 36 + } catch { 37 + return { valid: false }; 38 + } 39 + } 40 + return { valid: false }; 41 + } 42 + 43 + try { 44 + new URL(str); 45 + return { valid: true, url: str }; 46 + } catch { 47 + return { valid: false }; 48 + } 49 + } 50 + 51 + const commandDefinitions = [ 52 + { 53 + name: 'open', 54 + description: 'Open a URL in a new window', 55 + execute: async (ctx) => { 56 + const parts = ctx.typed.split(' '); 57 + parts.shift(); 58 + const address = parts.shift(); 59 + 60 + if (!address) { 61 + return { error: 'No address provided' }; 62 + } 63 + 64 + const urlResult = getValidURL(address); 65 + if (!urlResult.valid) { 66 + return { error: 'Invalid URL. Must be a valid URL starting with http://, https://, or other valid protocol.' }; 67 + } 68 + 69 + const normalizedAddress = urlResult.url; 70 + 71 + try { 72 + await api.window.open(normalizedAddress, { 73 + width: 1024, 74 + height: 768, 75 + trackingSource: 'cmd', 76 + trackingSourceId: 'open', 77 + }); 78 + 79 + return { 80 + command: 'open', 81 + address: normalizedAddress, 82 + success: true, 83 + }; 84 + } catch (error) { 85 + console.error('[page] Failed to open window:', error); 86 + return { 87 + error: 'Failed to open window: ' + error.message, 88 + address: normalizedAddress, 89 + }; 90 + } 91 + }, 92 + }, 93 + { 94 + name: 'modal', 95 + description: 'Open a URL in a modal window that hides on blur or escape', 96 + execute: async (ctx) => { 97 + const parts = ctx.typed.split(' '); 98 + parts.shift(); 99 + const address = parts.shift(); 100 + 101 + if (!address) { 102 + return { error: 'No address provided' }; 103 + } 104 + 105 + try { 106 + await api.window.openModal(address, { 107 + width: 700, 108 + height: 500, 109 + }); 110 + } catch (error) { 111 + console.error('[page] Failed to open modal window:', error); 112 + return { error: 'Failed to open modal window: ' + error.message }; 113 + } 114 + 115 + return { 116 + command: 'modal', 117 + address, 118 + }; 119 + }, 120 + }, 121 + ]; 122 + 123 + const registeredCommands = []; 124 + 125 + export const init = async () => { 126 + for (const cmd of commandDefinitions) { 127 + api.commands.register(cmd); 128 + registeredCommands.push(cmd.name); 129 + } 130 + }; 131 + 132 + export const uninit = async () => { 133 + for (const name of registeredCommands) { 134 + api.commands.unregister(name); 135 + } 136 + registeredCommands.length = 0; 137 + }; 138 + 139 + export { init as initPage, uninit as uninitPage };
+9 -11
backend/electron/main.ts
··· 13 13 import { initDatabase, closeDatabase, getDb, getContextEntry } from './datastore.js'; 14 14 import { registerScheme, initProtocol, registerExtensionPath, getExtensionPath, getRegisteredExtensionIds, registerThemePath, getRegisteredThemeIds } from './protocol.js'; 15 15 import { initHud } from './hud-glue.js'; 16 - import { initPage } from './page-glue.js'; 17 16 import { initCore, getCoreBackgroundWindow } from './core-glue.js'; 18 17 import { initTestFixture } from './test-fixture-glue.js'; 19 18 import { discoverExtensions, loadExtensionManifest, isBuiltinExtensionEnabled, type ExtensionManifest, type ManifestCommand, type ManifestShortcut } from './extensions.js'; ··· 73 72 // 74 73 // NOTE: 'cmd', 'hud', and 'page' were removed from this list when they were 75 74 // extracted from features/ into core app code (app/cmd/, app/hud/, app/page/). 76 - // cmd's registry + shortcut wiring now runs inside the core background 77 - // renderer (app/index.js calls initCmd() directly). hud and page still use 78 - // their own resident renderers (hud-glue / page-glue); those are pending 79 - // consolidation into core next. 75 + // cmd's registry + shortcut wiring and page's `open`/`modal` commands now 76 + // run inside the core background renderer (app/index.js calls initCmd() 77 + // and initPage() directly). hud still uses its own resident renderer 78 + // (hud-glue); pending consolidation into core next. 80 79 const CONSOLIDATED_EXTENSION_IDS = ['editor', 'groups', 'lex', 'lists', 'peeks', 'search', 'slides', 'spaces', 'websearch', 'windows', 'files', 'pagestream', 'sheets', 'tags', 'feeds', 'entities', 'scripts', 'timers', 'wonderwall']; 81 80 82 81 // Extensions that must load eagerly (not lazy) — needed at startup ··· 1074 1073 // grant) — no longer loaded as an iframe in the consolidated extension host. 1075 1074 await initHud({ tilePreloadPath }); 1076 1075 1077 - // Load page background right after hud — registers the `open` and `modal` 1078 - // commands. Page is core infrastructure (the canvas architecture in 1079 - // app/page/index.html + page.js is the primary web rendering surface). 1080 - // page now runs as a standalone tile renderer (tile-preload + trustedBuiltin 1081 - // grant) — no longer loaded as an iframe in the consolidated extension host. 1082 - await initPage({ tilePreloadPath }); 1076 + // page's `open` / `modal` command registration now runs inside the 1077 + // core background renderer (app/index.js awaits initPage() from 1078 + // app/page/background.js). No separate resident window — the per-URL 1079 + // canvas page windows are still created on demand by the cmd 1080 + // handlers calling api.window.open(...). 1083 1081 1084 1082 // Launch the privileged test-fixture renderer when running under 1085 1083 // Playwright (E2E_TEST=true). This is the v2 replacement for the v1
-147
backend/electron/page-glue.ts
··· 1 - /** 2 - * Electron-specific plumbing for the page subsystem. 3 - * 4 - * Page is NOT a feature — it's core app infrastructure. The cross-platform 5 - * page code (canvas UI in `app/page/index.html` + `page.js`, command 6 - * registration in `app/page/background.html`) lives in `app/page/`. This 7 - * file contains the Electron-specific glue: 8 - * 9 - * - Creates the hidden resident page BrowserWindow with `tile-preload.cjs` 10 - * and a `trustedBuiltin` capability grant. The resident renderer is 11 - * `peek://page/background.html`; it registers the `open` and `modal` 12 - * commands via the portable `api.commands.register()` surface. 13 - * 14 - * The canvas page architecture (fullscreen transparent BrowserWindow at 15 - * 0,0, peek://app/page/index.html?url=..., JS-positioned webview) is 16 - * implemented in `backend/electron/ipc.ts` and `app/page/page.js` — those 17 - * are NOT touched here. The canvas architecture is FROZEN (see 18 - * DEVELOPMENT.md). 19 - * 20 - * An eventual Tauri port would add backend/tauri/src-tauri/src/page_glue.rs 21 - * that performs the equivalent background-script load through Tauri's 22 - * WebView. 23 - */ 24 - 25 - import { BrowserWindow } from 'electron'; 26 - import { createRequire } from 'node:module'; 27 - import { DEBUG } from './config.js'; 28 - import { createTrustedBuiltinGrant } from './tile-manifest.js'; 29 - import { generateToken } from './tile-tokens.js'; 30 - import { registerTrustedBuiltinWindow } from './tile-launcher.js'; 31 - 32 - const requireElectron = createRequire(import.meta.url); 33 - 34 - /** 35 - * The tile id used to key the page resident renderer in the token store 36 - * and source-address namespace. Must match the `peek://page/...` origin 37 - * the protocol handler recognises. 38 - */ 39 - export const PAGE_ID = 'page'; 40 - 41 - /** 42 - * Entry id for the resident page renderer. Matches the shape used by 43 - * other tiles (tileId:entryId keying), even though page has only one 44 - * entry. 45 - */ 46 - export const PAGE_ENTRY_ID = 'resident'; 47 - 48 - /** 49 - * URL of the page resident renderer (served from app/page/ via 50 - * protocol.ts). The resident renderer is single-file: the inline module 51 - * script is the former `background.js` content. 52 - */ 53 - export const PAGE_RESIDENT_URL = 'peek://page/background.html'; 54 - 55 - let pageResidentWindow: BrowserWindow | null = null; 56 - 57 - /** 58 - * Options for initializing page. 59 - */ 60 - export interface InitPageOptions { 61 - /** Absolute path to `dist/backend/electron/tile-preload.cjs`. */ 62 - tilePreloadPath: string; 63 - } 64 - 65 - /** 66 - * Launch the page resident renderer as a hidden BrowserWindow that uses 67 - * `tile-preload.cjs` with a trustedBuiltin capability grant. 68 - * 69 - * The resident renderer registers the `open` and `modal` commands via 70 - * the portable `api.commands.register` surface. Capability enforcement 71 - * is bypassed at every `tile:*` IPC because the token's grant carries 72 - * `trustedBuiltin: true`. 73 - * 74 - * The renderer window is created hidden and never shown — page has no 75 - * UI of its own. The per-URL canvas windows are created independently 76 - * via `api.window.open(...)` from the cmd-issued `open`/`modal` commands. 77 - */ 78 - export async function initPage(opts: InitPageOptions): Promise<void> { 79 - DEBUG && console.log('[page-glue] Launching page resident renderer'); 80 - 81 - const electron = requireElectron('electron') as typeof import('electron'); 82 - const BrowserWindowCtor = electron.BrowserWindow; 83 - 84 - // Mint a token backed by a trustedBuiltin grant — every tile:* IPC the 85 - // resident renderer invokes will bypass capability enforcement. 86 - const grant = createTrustedBuiltinGrant(PAGE_ID); 87 - const token = generateToken(PAGE_ID, PAGE_ENTRY_ID, grant); 88 - 89 - const win = new BrowserWindowCtor({ 90 - width: 1, 91 - height: 1, 92 - show: false, 93 - frame: false, 94 - focusable: false, 95 - skipTaskbar: true, 96 - webPreferences: { 97 - sandbox: true, 98 - contextIsolation: true, 99 - nodeIntegration: false, 100 - preload: opts.tilePreloadPath, 101 - additionalArguments: [ 102 - `--tile-id=${PAGE_ID}`, 103 - `--tile-entry=${PAGE_ENTRY_ID}`, 104 - `--tile-token=${token}`, 105 - ], 106 - }, 107 - }); 108 - 109 - pageResidentWindow = win; 110 - 111 - // Register with the tile launcher so the main-process extensionBroadcaster 112 - // forwards pubsub messages to this window (same path as feature tiles). 113 - // The registration also wires the close/revoke cleanup. 114 - registerTrustedBuiltinWindow(PAGE_ID, PAGE_ENTRY_ID, win, token); 115 - 116 - win.webContents.on('console-message', (event) => { 117 - try { 118 - const level = parseInt(String((event as { level?: unknown }).level), 10); 119 - const message = String((event as { message?: unknown }).message ?? ''); 120 - const levelLabels = ['verbose', 'info', 'warning', 'error']; 121 - const levelLabel = levelLabels[level] || 'info'; 122 - if (level >= 2) { 123 - console.error(`[page:resident] [${levelLabel}] ${message}`); 124 - } else if (DEBUG) { 125 - console.log(`[page:resident] [${levelLabel}] ${message}`); 126 - } 127 - } catch { 128 - /* logging must never throw */ 129 - } 130 - }); 131 - 132 - win.on('closed', () => { 133 - pageResidentWindow = null; 134 - }); 135 - 136 - win.loadURL(PAGE_RESIDENT_URL); 137 - 138 - DEBUG && console.log('[page-glue] page resident renderer loaded'); 139 - } 140 - 141 - /** 142 - * Accessor for the resident page renderer window. Returns null if page 143 - * has not yet been initialized or the window was closed. 144 - */ 145 - export function getPageResidentWindow(): BrowserWindow | null { 146 - return pageResidentWindow; 147 - }