experiments in a post-browser web
10
fork

Configure Feed

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

feat(tile-ipc): tile:window:opener-* strict shims (Phase 3.5a)

Add tile:window:opener-postmessage, tile:window:opener-close, and
tile:window:opener-focus IPC handlers in tile-ipc.ts as strict
counterparts to the legacy opener-* channels in ipc.ts. Each handler
is gated by window.manage via checkWindowAllowed. Add corresponding
openerPostMessage(), openerClose(), openerFocus() wrapper methods in
tile-preload.cts (additive only — existing opener-* invokes untouched).
Export popupToOpener map from ipc.ts and extend WindowOp union type
plus checkWindowAllowed switch in tile-window-enforcement.ts.

+169 -4
+1 -1
backend/electron/ipc.ts
··· 289 289 // window.opener.postMessage() calls from popup webviews back to the opener. 290 290 // Populated in setWindowOpenHandler, cleaned up on popup window close. 291 291 292 - const popupToOpener = new Map<number, number>(); 292 + export const popupToOpener = new Map<number, number>(); 293 293 294 294 /** 295 295 * Convert an Item to an Address-like shape for backward compatibility.
+115 -1
backend/electron/tile-ipc.ts
··· 84 84 import { installFromBundle } from './feature-installer.js'; 85 85 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; 86 86 import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 87 - import { invokeWindowOpen } from './ipc.js'; 87 + import { invokeWindowOpen, popupToOpener } from './ipc.js'; 88 88 import { 89 89 registerGlobalShortcut, 90 90 unregisterGlobalShortcut, ··· 2957 2957 const message = err instanceof Error ? err.message : String(err); 2958 2958 return { success: false, error: message }; 2959 2959 } 2960 + }); 2961 + 2962 + // ── tile:window:opener-postmessage ─────────────────────────────── 2963 + // 2964 + // Strict counterpart of the legacy `opener-postmessage` channel. 2965 + // Routes a postMessage from a popup tile to its opener window. 2966 + // Gated by `window.manage`. 2967 + ipcMain.handle('tile:window:opener-postmessage', async (event, args: { 2968 + token: string; 2969 + message: unknown; 2970 + origin?: string; 2971 + }) => { 2972 + const grant = getGrantForToken(args.token); 2973 + const check = checkWindowAllowed(grant, 'opener-postmessage'); 2974 + if (!check.ok) { 2975 + handleViolation(grant, 'window', 'window:opener-postmessage', check.error, args.token); 2976 + return { success: false, error: check.error }; 2977 + } 2978 + 2979 + const senderWin = BrowserWindow.fromWebContents(event.sender); 2980 + if (!senderWin) return { success: false, error: 'No sender window' }; 2981 + 2982 + const openerWindowId = popupToOpener.get(senderWin.id); 2983 + if (!openerWindowId) { 2984 + DEBUG && console.log('[tile:window:opener-postmessage] No opener mapping for window', senderWin.id); 2985 + return { success: false, error: 'No opener window mapped' }; 2986 + } 2987 + 2988 + const openerWin = BrowserWindow.fromId(openerWindowId); 2989 + if (!openerWin || openerWin.isDestroyed()) { 2990 + DEBUG && console.log('[tile:window:opener-postmessage] Opener window', openerWindowId, 'not found or destroyed'); 2991 + popupToOpener.delete(senderWin.id); 2992 + return { success: false, error: 'Opener window not found' }; 2993 + } 2994 + 2995 + openerWin.webContents.send('opener-message-received', { 2996 + message: args.message, 2997 + origin: args.origin || '*', 2998 + popupWindowId: senderWin.id, 2999 + }); 3000 + 3001 + DEBUG && console.log(`[tile:window:opener-postmessage] Routed postMessage from popup ${senderWin.id} to opener ${openerWindowId}`); 3002 + return { success: true }; 3003 + }); 3004 + 3005 + // ── tile:window:opener-close ────────────────────────────────────── 3006 + // 3007 + // Strict counterpart of the legacy `opener-close` channel. 3008 + // Closes the opener window from a popup tile. 3009 + // Gated by `window.manage`. 3010 + ipcMain.handle('tile:window:opener-close', async (event, args: { 3011 + token: string; 3012 + }) => { 3013 + const grant = getGrantForToken(args.token); 3014 + const check = checkWindowAllowed(grant, 'opener-close'); 3015 + if (!check.ok) { 3016 + handleViolation(grant, 'window', 'window:opener-close', check.error, args.token); 3017 + return { success: false, error: check.error }; 3018 + } 3019 + 3020 + const senderWin = BrowserWindow.fromWebContents(event.sender); 3021 + if (!senderWin) return { success: false, error: 'No sender window' }; 3022 + 3023 + const openerWindowId = popupToOpener.get(senderWin.id); 3024 + if (!openerWindowId) return { success: false, error: 'No opener mapped' }; 3025 + 3026 + const openerWin = BrowserWindow.fromId(openerWindowId); 3027 + if (!openerWin || openerWin.isDestroyed()) { 3028 + popupToOpener.delete(senderWin.id); 3029 + return { success: false, error: 'Opener window not found' }; 3030 + } 3031 + 3032 + try { 3033 + const { closeOrHideWindow } = await import('./windows.js'); 3034 + DEBUG && console.log(`[tile:window:opener-close] Popup ${senderWin.id} closing opener ${openerWindowId}`); 3035 + closeOrHideWindow(openerWin.id); 3036 + return { success: true }; 3037 + } catch (err) { 3038 + const message = err instanceof Error ? err.message : String(err); 3039 + return { success: false, error: message }; 3040 + } 3041 + }); 3042 + 3043 + // ── tile:window:opener-focus ────────────────────────────────────── 3044 + // 3045 + // Strict counterpart of the legacy `opener-focus` channel. 3046 + // Focuses the opener window from a popup tile. 3047 + // Gated by `window.manage`. 3048 + ipcMain.handle('tile:window:opener-focus', async (event, args: { 3049 + token: string; 3050 + }) => { 3051 + const grant = getGrantForToken(args.token); 3052 + const check = checkWindowAllowed(grant, 'opener-focus'); 3053 + if (!check.ok) { 3054 + handleViolation(grant, 'window', 'window:opener-focus', check.error, args.token); 3055 + return { success: false, error: check.error }; 3056 + } 3057 + 3058 + const senderWin = BrowserWindow.fromWebContents(event.sender); 3059 + if (!senderWin) return { success: false, error: 'No sender window' }; 3060 + 3061 + const openerWindowId = popupToOpener.get(senderWin.id); 3062 + if (!openerWindowId) return { success: false, error: 'No opener mapped' }; 3063 + 3064 + const openerWin = BrowserWindow.fromId(openerWindowId); 3065 + if (!openerWin || openerWin.isDestroyed()) { 3066 + popupToOpener.delete(senderWin.id); 3067 + return { success: false, error: 'Opener window not found' }; 3068 + } 3069 + 3070 + DEBUG && console.log(`[tile:window:opener-focus] Popup ${senderWin.id} focusing opener ${openerWindowId}`); 3071 + openerWin.show(); 3072 + openerWin.focus(); 3073 + return { success: true }; 2960 3074 }); 2961 3075 2962 3076 // ── tile:screen:get-primary-display ───────────────────────────────
+42
backend/electron/tile-preload.cts
··· 782 782 token: tileToken, 783 783 }); 784 784 }, 785 + 786 + /** 787 + * Send a postMessage from this popup tile to its opener window. 788 + * 789 + * Strict path routes through `tile:window:opener-postmessage` which 790 + * requires `window.manage`. Intended for popup tiles that communicate 791 + * results back to the window that opened them. 792 + */ 793 + openerPostMessage: (message: unknown, origin?: string) => { 794 + if (!tokenValid) return Promise.reject(new Error('Not initialized')); 795 + return ipcRenderer.invoke('tile:window:opener-postmessage', { 796 + token: tileToken, 797 + message, 798 + origin, 799 + }); 800 + }, 801 + 802 + /** 803 + * Close the opener window from this popup tile. 804 + * 805 + * Strict path routes through `tile:window:opener-close` which 806 + * requires `window.manage`. 807 + */ 808 + openerClose: () => { 809 + if (!tokenValid) return Promise.reject(new Error('Not initialized')); 810 + return ipcRenderer.invoke('tile:window:opener-close', { 811 + token: tileToken, 812 + }); 813 + }, 814 + 815 + /** 816 + * Focus the opener window from this popup tile. 817 + * 818 + * Strict path routes through `tile:window:opener-focus` which 819 + * requires `window.manage`. 820 + */ 821 + openerFocus: () => { 822 + if (!tokenValid) return Promise.reject(new Error('Not initialized')); 823 + return ipcRenderer.invoke('tile:window:opener-focus', { 824 + token: tileToken, 825 + }); 826 + }, 785 827 }; 786 828 787 829 // ── Datastore (if granted) ──
+11 -2
backend/electron/tile-window-enforcement.ts
··· 40 40 * - `get-focused-visible-id` — read lastFocusedVisibleWindowId; gated by `query`. 41 41 * - `set-overlay-focus-target` — set overlay restore target; gated by `manage`. 42 42 * - `set-visible-on-all-workspaces` — pin window across macOS Spaces; gated by `manage`. 43 + * - `opener-postmessage` — postMessage from a popup to its opener; gated by `manage`. 44 + * - `opener-close` — close the opener window from a popup; gated by `manage`. 45 + * - `opener-focus` — focus the opener window from a popup; gated by `manage`. 43 46 */ 44 47 export type WindowOp = 45 48 | 'open' ··· 56 59 | 'fullscreen' 57 60 | 'get-focused-visible-id' 58 61 | 'set-overlay-focus-target' 59 - | 'set-visible-on-all-workspaces'; 62 + | 'set-visible-on-all-workspaces' 63 + | 'opener-postmessage' 64 + | 'opener-close' 65 + | 'opener-focus'; 60 66 61 67 export type WindowCheckResult = 62 68 | { ok: true } ··· 158 164 case 'maximize': 159 165 case 'fullscreen': 160 166 case 'set-overlay-focus-target': 161 - case 'set-visible-on-all-workspaces': { 167 + case 'set-visible-on-all-workspaces': 168 + case 'opener-postmessage': 169 + case 'opener-close': 170 + case 'opener-focus': { 162 171 // Mutating / probing an existing window by id is treated as a 163 172 // management operation. Gated by `manage`. 164 173 if (cap.manage !== true) {