experiments in a post-browser web
10
fork

Configure Feed

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

refactor(context): delete dead app/context + collapse fallbacks (Phase 3.8b)

- Delete app/context/{index,history,store}.js — dead code confirmed by 3.8a
audit (no renderer ever imported them; context.init() was never called)
- Delete contextCompat shim + hasContextCapability() from tile-preload.cts;
all six ternary compat branches collapsed to direct contextStrict calls
(createTrustedBuiltinGrant already carries context capability for cmd/hud/page)
- Delete registerContextHandlers() from ipc.ts and its call site — removes
the six legacy context-{get,set,history,snapshot,windows-with-value,
windows-in-space} ipcMain.handle blocks that were only reachable via the
now-deleted compat path
- TypeScript check clean; yarn test:unit skipped (no node_modules in worktree)

+20 -763
-122
app/context/history.js
··· 1 - /** 2 - * Context History - Persistence helpers for context changes 3 - * 4 - * Handles storing and querying context history from the backend datastore. 5 - * Used for time-series visualization and correlation with visits. 6 - */ 7 - 8 - /** 9 - * @typedef {Object} ContextHistoryEntry 10 - * @property {string} id - Unique entry ID 11 - * @property {string} key - Context key 12 - * @property {*} value - The context value 13 - * @property {Object} metadata - Additional context-specific data 14 - * @property {number|null} windowId - Window ID 15 - * @property {string} source - Source that set this context 16 - * @property {number} timestamp - Unix timestamp ms 17 - * @property {string|null} prevEntryId - Previous entry ID for chaining 18 - */ 19 - 20 - /** 21 - * @typedef {Object} HistoryQueryOptions 22 - * @property {number} [since] - Start timestamp (inclusive) 23 - * @property {number} [until] - End timestamp (inclusive) 24 - * @property {number|null} [windowId] - Filter by window ID 25 - * @property {number} [limit] - Max entries to return 26 - * @property {string} [order='desc'] - Sort order ('asc' or 'desc') 27 - */ 28 - 29 - // Reference to the IPC renderer (set during initialization) 30 - let ipcRenderer = null; 31 - 32 - /** 33 - * Initialize the history module with IPC renderer 34 - * @param {object} renderer - Electron IPC renderer 35 - */ 36 - export function init(renderer) { 37 - ipcRenderer = renderer; 38 - } 39 - 40 - /** 41 - * Save a context entry to history 42 - * @param {string} key - Context key 43 - * @param {*} value - Context value 44 - * @param {Object} options - Options 45 - * @param {Object} [options.metadata={}] - Additional metadata 46 - * @param {number|null} [options.windowId=null] - Window ID 47 - * @param {string} [options.source=''] - Source identifier 48 - * @returns {Promise<{success: boolean, id?: string, error?: string}>} 49 - */ 50 - export async function save(key, value, options = {}) { 51 - if (!ipcRenderer) { 52 - console.warn('[context:history] IPC not initialized'); 53 - return { success: false, error: 'IPC not initialized' }; 54 - } 55 - 56 - return ipcRenderer.invoke('context-set', { 57 - key, 58 - value, 59 - metadata: options.metadata || {}, 60 - windowId: options.windowId ?? null, 61 - source: options.source || '' 62 - }); 63 - } 64 - 65 - /** 66 - * Query context history 67 - * @param {string} key - Context key 68 - * @param {HistoryQueryOptions} options - Query options 69 - * @returns {Promise<{success: boolean, data?: ContextHistoryEntry[], error?: string}>} 70 - */ 71 - export async function query(key, options = {}) { 72 - if (!ipcRenderer) { 73 - console.warn('[context:history] IPC not initialized'); 74 - return { success: false, error: 'IPC not initialized' }; 75 - } 76 - 77 - return ipcRenderer.invoke('context-history', { 78 - key, 79 - ...options 80 - }); 81 - } 82 - 83 - /** 84 - * Get the latest entry for a key from history 85 - * @param {string} key - Context key 86 - * @param {number|null} windowId - Optional window ID filter 87 - * @returns {Promise<{success: boolean, data?: ContextHistoryEntry, error?: string}>} 88 - */ 89 - export async function getLatest(key, windowId = null) { 90 - const result = await query(key, { windowId, limit: 1, order: 'desc' }); 91 - if (result.success && result.data && result.data.length > 0) { 92 - return { success: true, data: result.data[0] }; 93 - } 94 - return { success: true, data: null }; 95 - } 96 - 97 - /** 98 - * Get context snapshot at a specific time 99 - * Returns the most recent entry for each key before the given timestamp 100 - * @param {number} timestamp - Point in time 101 - * @param {string[]} keys - Keys to include (empty = all keys) 102 - * @returns {Promise<{success: boolean, data?: Object.<string, ContextHistoryEntry>, error?: string}>} 103 - */ 104 - export async function getSnapshot(timestamp, keys = []) { 105 - if (!ipcRenderer) { 106 - console.warn('[context:history] IPC not initialized'); 107 - return { success: false, error: 'IPC not initialized' }; 108 - } 109 - 110 - return ipcRenderer.invoke('context-snapshot', { 111 - timestamp, 112 - keys 113 - }); 114 - } 115 - 116 - export default { 117 - init, 118 - save, 119 - query, 120 - getLatest, 121 - getSnapshot 122 - };
-204
app/context/index.js
··· 1 - /** 2 - * Context API - App-wide context management 3 - * 4 - * Provides a simple key-value store for application context with: 5 - * - Per-window and global context 6 - * - Change notifications via watch() 7 - * - Time-series history for visualization 8 - * 9 - * "mode" is just one key in this general-purpose system. 10 - * 11 - * Usage: 12 - * import context from 'peek://app/context/index.js'; 13 - * 14 - * // Set context 15 - * context.set('mode', 'page', { metadata: { url: 'https://example.com' } }); 16 - * 17 - * // Get context 18 - * const entry = context.get('mode'); 19 - * // { value: 'page', metadata: { url: '...' }, timestamp: ... } 20 - * 21 - * // Watch for changes 22 - * const unsub = context.watch('mode', (entry, oldEntry) => { 23 - * console.log('Mode changed to:', entry?.value); 24 - * }); 25 - * // Later: unsub(); 26 - * 27 - * // Query history 28 - * const history = await context.getHistory('mode', { since: startOfDay }); 29 - */ 30 - 31 - import * as store from './store.js'; 32 - import * as history from './history.js'; 33 - 34 - // Re-export store functions for direct use 35 - export const get = store.get; 36 - export const watch = store.watch; 37 - export const remove = store.remove; 38 - export const getAll = store.getAll; 39 - export const clearWindow = store.clearWindow; 40 - export const getWindowsWithContext = store.getWindowsWithContext; 41 - export const getWindowsMatching = store.getWindowsMatching; 42 - 43 - // IPC renderer reference (set during init) 44 - let ipcRenderer = null; 45 - 46 - /** 47 - * Initialize the context module 48 - * @param {object} renderer - Electron IPC renderer 49 - */ 50 - export function init(renderer) { 51 - ipcRenderer = renderer; 52 - history.init(renderer); 53 - } 54 - 55 - /** 56 - * Set a context value with persistence 57 - * @param {string} key - Context key 58 - * @param {*} value - Value to set 59 - * @param {Object} options - Options 60 - * @param {Object} [options.metadata={}] - Additional metadata 61 - * @param {string} [options.source=''] - Source identifier (defaults to window.location) 62 - * @param {number|null} [options.windowId=null] - Window ID 63 - * @param {boolean} [options.persist=true] - Whether to persist to history 64 - * @returns {Promise<{success: boolean, entry?: Object, error?: string}>} 65 - */ 66 - export async function set(key, value, options = {}) { 67 - const { 68 - metadata = {}, 69 - source = typeof window !== 'undefined' ? window.location.toString() : '', 70 - windowId = null, 71 - persist = true 72 - } = options; 73 - 74 - // Update in-memory store 75 - const entry = store.set(key, value, { metadata, source, windowId }); 76 - 77 - // Persist to backend (async, don't block) 78 - if (persist && ipcRenderer) { 79 - try { 80 - const result = await ipcRenderer.invoke('context-set', { 81 - key, 82 - value, 83 - metadata, 84 - windowId, 85 - source 86 - }); 87 - if (!result.success) { 88 - console.warn('[context] Failed to persist:', result.error); 89 - } 90 - } catch (err) { 91 - console.error('[context] Persistence error:', err); 92 - } 93 - } 94 - 95 - return { success: true, entry }; 96 - } 97 - 98 - /** 99 - * Query context history 100 - * @param {string} key - Context key 101 - * @param {Object} options - Query options 102 - * @param {number} [options.since] - Start timestamp 103 - * @param {number} [options.until] - End timestamp 104 - * @param {number|null} [options.windowId] - Filter by window 105 - * @param {number} [options.limit] - Max entries 106 - * @returns {Promise<{success: boolean, data?: Array, error?: string}>} 107 - */ 108 - export async function getHistory(key, options = {}) { 109 - return history.query(key, options); 110 - } 111 - 112 - /** 113 - * Get context snapshot at a point in time 114 - * @param {number} timestamp - Point in time 115 - * @param {string[]} keys - Keys to include 116 - * @returns {Promise<{success: boolean, data?: Object, error?: string}>} 117 - */ 118 - export async function getSnapshot(timestamp, keys = []) { 119 - return history.getSnapshot(timestamp, keys); 120 - } 121 - 122 - // Mode-specific helpers (convenience functions) 123 - 124 - /** 125 - * Get the current mode for a window 126 - * @param {number|null} windowId - Window ID (null for global) 127 - * @returns {string|null} Mode value or null 128 - */ 129 - export function getMode(windowId = null) { 130 - const entry = store.get('mode', windowId); 131 - return entry?.value ?? null; 132 - } 133 - 134 - /** 135 - * Set the mode for a window 136 - * @param {string} mode - Mode value ('default', 'page', 'space', etc.) 137 - * @param {Object} options - Options 138 - * @param {Object} [options.metadata={}] - Mode metadata (e.g., spaceId, spaceName) 139 - * @param {number|null} [options.windowId=null] - Window ID 140 - * @returns {Promise<{success: boolean, entry?: Object, error?: string}>} 141 - */ 142 - export function setMode(mode, options = {}) { 143 - return set('mode', mode, options); 144 - } 145 - 146 - /** 147 - * Watch for mode changes 148 - * @param {function(string, Object): void} callback - Called with (modeValue, entry) 149 - * @param {number|null} windowId - Window ID to watch (null watches all) 150 - * @returns {function(): void} Unsubscribe function 151 - */ 152 - export function watchMode(callback, windowId = null) { 153 - return store.watch('mode', (entry, oldEntry) => { 154 - // Filter by windowId if specified 155 - if (windowId !== null && entry?.windowId !== windowId) { 156 - return; 157 - } 158 - callback(entry?.value ?? null, entry); 159 - }); 160 - } 161 - 162 - /** 163 - * Get all windows in a specific mode 164 - * @param {string} mode - Mode value to match 165 - * @returns {number[]} Array of window IDs 166 - */ 167 - export function getWindowsInMode(mode) { 168 - return store.getWindowsWithContext('mode', mode); 169 - } 170 - 171 - /** 172 - * Get all windows in a specific space 173 - * @param {string} spaceId - Space ID to match 174 - * @returns {number[]} Array of window IDs 175 - */ 176 - export function getWindowsInSpace(spaceId) { 177 - return store.getWindowsMatching('mode', (entry) => { 178 - return entry.value === 'space' && entry.metadata?.spaceId === spaceId; 179 - }); 180 - } 181 - 182 - export default { 183 - // Core API 184 - init, 185 - get, 186 - set, 187 - remove, 188 - watch, 189 - getAll, 190 - clearWindow, 191 - getHistory, 192 - getSnapshot, 193 - 194 - // Query helpers 195 - getWindowsWithContext, 196 - getWindowsMatching, 197 - 198 - // Mode-specific helpers 199 - getMode, 200 - setMode, 201 - watchMode, 202 - getWindowsInMode, 203 - getWindowsInSpace 204 - };
-223
app/context/store.js
··· 1 - /** 2 - * Context Store - In-memory state management for window context 3 - * 4 - * Manages per-window context state with pub/sub for change notifications. 5 - * This is the frontend-side cache; backend persists to context_history table. 6 - */ 7 - 8 - /** 9 - * @typedef {Object} ContextEntry 10 - * @property {string} key - Context key (e.g., 'mode') 11 - * @property {*} value - The context value 12 - * @property {Object} metadata - Additional context-specific data 13 - * @property {number} timestamp - Unix timestamp ms when set 14 - * @property {string} source - Source that set this context 15 - * @property {number|null} windowId - Window ID (null for global) 16 - */ 17 - 18 - // Per-window context storage 19 - // Map<windowId, Map<key, ContextEntry>> 20 - const windowContexts = new Map(); 21 - 22 - // Global context (windowId = null) 23 - const globalContext = new Map(); 24 - 25 - // Subscribers for context changes 26 - // Map<key, Set<callback>> 27 - const subscribers = new Map(); 28 - 29 - /** 30 - * Get the context map for a window 31 - * @param {number|null} windowId - Window ID or null for global 32 - * @returns {Map<string, ContextEntry>} 33 - */ 34 - function getContextMap(windowId) { 35 - if (windowId === null || windowId === undefined) { 36 - return globalContext; 37 - } 38 - if (!windowContexts.has(windowId)) { 39 - windowContexts.set(windowId, new Map()); 40 - } 41 - return windowContexts.get(windowId); 42 - } 43 - 44 - /** 45 - * Get a context entry 46 - * @param {string} key - Context key 47 - * @param {number|null} windowId - Window ID (null for global) 48 - * @returns {ContextEntry|null} 49 - */ 50 - export function get(key, windowId = null) { 51 - const contextMap = getContextMap(windowId); 52 - const entry = contextMap.get(key); 53 - return entry ? { ...entry } : null; 54 - } 55 - 56 - /** 57 - * Set a context value 58 - * @param {string} key - Context key 59 - * @param {*} value - Value to set 60 - * @param {Object} options - Options 61 - * @param {Object} [options.metadata={}] - Additional metadata 62 - * @param {string} [options.source=''] - Source identifier 63 - * @param {number|null} [options.windowId=null] - Window ID 64 - * @returns {ContextEntry} The created entry 65 - */ 66 - export function set(key, value, options = {}) { 67 - const { 68 - metadata = {}, 69 - source = '', 70 - windowId = null 71 - } = options; 72 - 73 - const contextMap = getContextMap(windowId); 74 - const oldEntry = contextMap.get(key); 75 - 76 - const entry = { 77 - key, 78 - value, 79 - metadata, 80 - timestamp: Date.now(), 81 - source, 82 - windowId 83 - }; 84 - 85 - contextMap.set(key, entry); 86 - 87 - // Notify subscribers 88 - notifySubscribers(key, entry, oldEntry); 89 - 90 - return { ...entry }; 91 - } 92 - 93 - /** 94 - * Delete a context entry 95 - * @param {string} key - Context key 96 - * @param {number|null} windowId - Window ID 97 - * @returns {boolean} True if entry existed and was deleted 98 - */ 99 - export function remove(key, windowId = null) { 100 - const contextMap = getContextMap(windowId); 101 - const oldEntry = contextMap.get(key); 102 - 103 - if (oldEntry) { 104 - contextMap.delete(key); 105 - notifySubscribers(key, null, oldEntry); 106 - return true; 107 - } 108 - return false; 109 - } 110 - 111 - /** 112 - * Watch for context changes 113 - * @param {string} key - Context key to watch 114 - * @param {function(ContextEntry|null, ContextEntry|null): void} callback - Called with (newEntry, oldEntry) 115 - * @returns {function(): void} Unsubscribe function 116 - */ 117 - export function watch(key, callback) { 118 - if (!subscribers.has(key)) { 119 - subscribers.set(key, new Set()); 120 - } 121 - subscribers.get(key).add(callback); 122 - 123 - // Return unsubscribe function 124 - return () => { 125 - const subs = subscribers.get(key); 126 - if (subs) { 127 - subs.delete(callback); 128 - if (subs.size === 0) { 129 - subscribers.delete(key); 130 - } 131 - } 132 - }; 133 - } 134 - 135 - /** 136 - * Notify subscribers of a context change 137 - * @param {string} key - Context key that changed 138 - * @param {ContextEntry|null} newEntry - New entry (null if removed) 139 - * @param {ContextEntry|null} oldEntry - Previous entry (null if new) 140 - */ 141 - function notifySubscribers(key, newEntry, oldEntry) { 142 - const subs = subscribers.get(key); 143 - if (subs) { 144 - for (const callback of subs) { 145 - try { 146 - callback(newEntry, oldEntry); 147 - } catch (err) { 148 - console.error('[context:store] Subscriber error:', err); 149 - } 150 - } 151 - } 152 - } 153 - 154 - /** 155 - * Get all context entries for a window 156 - * @param {number|null} windowId - Window ID 157 - * @returns {ContextEntry[]} 158 - */ 159 - export function getAll(windowId = null) { 160 - const contextMap = getContextMap(windowId); 161 - return Array.from(contextMap.values()).map(e => ({ ...e })); 162 - } 163 - 164 - /** 165 - * Clear all context for a window 166 - * Called when window closes 167 - * @param {number} windowId - Window ID 168 - */ 169 - export function clearWindow(windowId) { 170 - const contextMap = windowContexts.get(windowId); 171 - if (contextMap) { 172 - // Notify subscribers of removal for each key 173 - for (const [key, entry] of contextMap) { 174 - notifySubscribers(key, null, entry); 175 - } 176 - windowContexts.delete(windowId); 177 - } 178 - } 179 - 180 - /** 181 - * Get all windows with a specific context value 182 - * @param {string} key - Context key 183 - * @param {*} value - Value to match 184 - * @returns {number[]} Array of window IDs 185 - */ 186 - export function getWindowsWithContext(key, value) { 187 - const windowIds = []; 188 - for (const [windowId, contextMap] of windowContexts) { 189 - const entry = contextMap.get(key); 190 - if (entry && entry.value === value) { 191 - windowIds.push(windowId); 192 - } 193 - } 194 - return windowIds; 195 - } 196 - 197 - /** 198 - * Get all windows with context matching a predicate 199 - * @param {string} key - Context key 200 - * @param {function(ContextEntry): boolean} predicate - Filter function 201 - * @returns {number[]} Array of window IDs 202 - */ 203 - export function getWindowsMatching(key, predicate) { 204 - const windowIds = []; 205 - for (const [windowId, contextMap] of windowContexts) { 206 - const entry = contextMap.get(key); 207 - if (entry && predicate(entry)) { 208 - windowIds.push(windowId); 209 - } 210 - } 211 - return windowIds; 212 - } 213 - 214 - export default { 215 - get, 216 - set, 217 - remove, 218 - watch, 219 - getAll, 220 - clearWindow, 221 - getWindowsWithContext, 222 - getWindowsMatching 223 - };
-137
backend/electron/ipc.ts
··· 5118 5118 }); 5119 5119 } 5120 5120 5121 - /** 5122 - * Register context-related IPC handlers 5123 - * 5124 - * The context API provides a general-purpose key-value store for application context. 5125 - * "mode" is just one key in this system - extensions can define their own context keys. 5126 - */ 5127 - export function registerContextHandlers(): void { 5128 - // Get current context entry for a key 5129 - ipcMain.handle('context-get', async (ev, data: { key: string; windowId?: number | null }) => { 5130 - try { 5131 - let windowId = data.windowId; 5132 - 5133 - // If no windowId provided (or null from preload default), use calling window 5134 - if (windowId == null) { 5135 - const callingWin = BrowserWindow.fromWebContents(ev.sender); 5136 - windowId = callingWin?.id ?? null; 5137 - } 5138 - 5139 - const entry = getContextEntry(data.key, windowId); 5140 - return { success: true, data: entry }; 5141 - } catch (error) { 5142 - const message = error instanceof Error ? error.message : String(error); 5143 - return { success: false, error: message }; 5144 - } 5145 - }); 5146 - 5147 - // Set context value (persists to history) 5148 - ipcMain.handle('context-set', async (ev, data: { 5149 - key: string; 5150 - value: unknown; 5151 - metadata?: Record<string, unknown>; 5152 - windowId?: number | null; 5153 - source?: string; 5154 - }) => { 5155 - try { 5156 - let windowId = data.windowId; 5157 - let source = data.source; 5158 - 5159 - // If no windowId provided (or null from preload default), use calling window 5160 - if (windowId == null) { 5161 - const callingWin = BrowserWindow.fromWebContents(ev.sender); 5162 - windowId = callingWin?.id ?? null; 5163 - } 5164 - 5165 - // If no source provided, try to get from calling window's URL 5166 - if (!source) { 5167 - const callingWin = BrowserWindow.fromWebContents(ev.sender); 5168 - source = callingWin?.webContents.getURL() ?? ''; 5169 - } 5170 - 5171 - const result = addContextEntry(data.key, data.value, { 5172 - metadata: data.metadata, 5173 - windowId, 5174 - source 5175 - }); 5176 - 5177 - // Publish context change event for watchers 5178 - if (publish && PubSubScopes && getSystemAddress) { 5179 - publish(getSystemAddress(), PubSubScopes.GLOBAL, 'context:changed', { 5180 - key: data.key, 5181 - value: data.value, 5182 - metadata: data.metadata || {}, 5183 - windowId, 5184 - source, 5185 - entryId: result.id 5186 - }); 5187 - } 5188 - 5189 - return { success: true, data: result }; 5190 - } catch (error) { 5191 - const message = error instanceof Error ? error.message : String(error); 5192 - return { success: false, error: message }; 5193 - } 5194 - }); 5195 - 5196 - // Query context history 5197 - ipcMain.handle('context-history', async (ev, data: { 5198 - key?: string; 5199 - windowId?: number | null; 5200 - since?: number; 5201 - until?: number; 5202 - limit?: number; 5203 - order?: 'asc' | 'desc'; 5204 - }) => { 5205 - try { 5206 - const entries = queryContextHistory(data); 5207 - return { success: true, data: entries }; 5208 - } catch (error) { 5209 - const message = error instanceof Error ? error.message : String(error); 5210 - return { success: false, error: message }; 5211 - } 5212 - }); 5213 - 5214 - // Get context snapshot at a point in time 5215 - ipcMain.handle('context-snapshot', async (ev, data: { 5216 - timestamp: number; 5217 - keys?: string[]; 5218 - }) => { 5219 - try { 5220 - const snapshot = getContextSnapshot(data.timestamp, data.keys || []); 5221 - return { success: true, data: snapshot }; 5222 - } catch (error) { 5223 - const message = error instanceof Error ? error.message : String(error); 5224 - return { success: false, error: message }; 5225 - } 5226 - }); 5227 - 5228 - // Get all windows with a specific context value 5229 - ipcMain.handle('context-windows-with-value', async (ev, data: { 5230 - key: string; 5231 - value: unknown; 5232 - }) => { 5233 - try { 5234 - const windowIds = getWindowsWithContextValue(data.key, data.value); 5235 - return { success: true, data: windowIds }; 5236 - } catch (error) { 5237 - const message = error instanceof Error ? error.message : String(error); 5238 - return { success: false, error: message }; 5239 - } 5240 - }); 5241 - 5242 - // Get windows in a specific space (convenience method for mode='space' with spaceId) 5243 - ipcMain.handle('context-windows-in-space', async (ev, data: { spaceId: string }) => { 5244 - try { 5245 - const windowIds = getWindowsMatchingContext('mode', (entry) => { 5246 - return entry.value === 'space' && entry.metadata?.spaceId === data.spaceId; 5247 - }); 5248 - return { success: true, data: windowIds }; 5249 - } catch (error) { 5250 - const message = error instanceof Error ? error.message : String(error); 5251 - return { success: false, error: message }; 5252 - } 5253 - }); 5254 - 5255 - DEBUG && console.log('[ipc] Context handlers registered'); 5256 - } 5257 5121 5258 5122 /** 5259 5123 * Persist adBlockerEnabled pref in the datastore. ··· 5583 5447 registerBackupHandlers(); 5584 5448 registerProfileHandlers(); 5585 5449 registerModesHandlers(); 5586 - registerContextHandlers(); 5587 5450 registerWebExtensionHandlers(); 5588 5451 registerSessionHandlers(); 5589 5452 registerMiscHandlers(onQuit);
+20 -77
backend/electron/tile-preload.cts
··· 1303 1303 1304 1304 // ── Context ─────────────────────────────────────────────────────── 1305 1305 // 1306 - // Dual-path implementation mirroring api.shortcuts: 1307 - // 1308 - // - STRICT: when the tile's manifest declared a `context` capability 1309 - // (`true` or `{ read?, write?, modes?, queryWindows? }`), route 1310 - // through `tile:context:*`. Main-process handlers validate the 1311 - // token + capability shape + per-op gates. 1312 - // 1313 - // - V1-COMPAT: when no context capability was declared, fall back 1314 - // to the legacy `context-*` IPC channels. Those remain available 1315 - // through the Phase 3/4 migration window. 1316 - // 1317 - // The decision is made per-call because `grantedCapabilities` is 1318 - // populated asynchronously by `initialize()`. 1319 - 1320 - function hasContextCapability(): boolean { 1321 - const cc = grantedCapabilities?.context; 1322 - if (cc === true) return true; 1323 - if (cc && typeof cc === 'object') return true; 1324 - return false; 1325 - } 1326 - 1327 - const contextCompat = { 1328 - get: (key: string, windowId: number | null = null) => 1329 - ipcRenderer.invoke('context-get', { key, windowId }), 1330 - set: (key: string, value: unknown, metadata?: unknown, windowId?: number | null) => 1331 - ipcRenderer.invoke('context-set', { key, value, metadata, windowId }), 1332 - history: (key: string, limit?: number) => 1333 - ipcRenderer.invoke('context-history', { key, limit }), 1334 - snapshot: (windowId?: number | null) => 1335 - ipcRenderer.invoke('context-snapshot', { windowId }), 1336 - windowsWithValue: (key: string, value: unknown) => 1337 - ipcRenderer.invoke('context-windows-with-value', { key, value }), 1338 - windowsInSpace: (spaceId: string) => 1339 - ipcRenderer.invoke('context-windows-in-space', { spaceId }), 1340 - }; 1306 + // All renderers using tile-preload have `context` in their capability 1307 + // grant (feature tiles via manifest; cmd/hud/page via 1308 + // createTrustedBuiltinGrant). Route through tile:context:* strictly. 1341 1309 1342 1310 const contextStrict = { 1343 1311 get: (key: string, windowId: number | null = null) => ··· 1355 1323 }; 1356 1324 1357 1325 api.context = { 1358 - get: (key: unknown, windowId: unknown) => { 1359 - return hasContextCapability() 1360 - ? contextStrict.get(key as string, (windowId ?? null) as number | null) 1361 - : contextCompat.get(key as string, (windowId ?? null) as number | null); 1362 - }, 1363 - set: (key: unknown, value: unknown, metadata: unknown, windowId: unknown) => { 1364 - return hasContextCapability() 1365 - ? contextStrict.set(key as string, value, metadata as Record<string, unknown> | undefined, (windowId ?? null) as number | null) 1366 - : contextCompat.set(key as string, value, metadata, (windowId ?? null) as number | null); 1367 - }, 1368 - history: (key: unknown, limit: unknown) => { 1369 - return hasContextCapability() 1370 - ? contextStrict.history(key as string, limit as number | undefined) 1371 - : contextCompat.history(key as string, limit as number | undefined); 1372 - }, 1373 - snapshot: (windowId: unknown) => { 1374 - return hasContextCapability() 1375 - ? contextStrict.snapshot((windowId ?? null) as number | null) 1376 - : contextCompat.snapshot((windowId ?? null) as number | null); 1377 - }, 1378 - windowsWithValue: (key: unknown, value: unknown) => { 1379 - return hasContextCapability() 1380 - ? contextStrict.windowsWithValue(key as string, value) 1381 - : contextCompat.windowsWithValue(key as string, value); 1382 - }, 1383 - windowsInSpace: (spaceId: unknown) => { 1384 - return hasContextCapability() 1385 - ? contextStrict.windowsInSpace(spaceId as string) 1386 - : contextCompat.windowsInSpace(spaceId as string); 1387 - }, 1326 + get: (key: unknown, windowId: unknown) => 1327 + contextStrict.get(key as string, (windowId ?? null) as number | null), 1328 + set: (key: unknown, value: unknown, metadata: unknown, windowId: unknown) => 1329 + contextStrict.set(key as string, value, metadata as Record<string, unknown> | undefined, (windowId ?? null) as number | null), 1330 + history: (key: unknown, limit: unknown) => 1331 + contextStrict.history(key as string, limit as number | undefined), 1332 + snapshot: (windowId: unknown) => 1333 + contextStrict.snapshot((windowId ?? null) as number | null), 1334 + windowsWithValue: (key: unknown, value: unknown) => 1335 + contextStrict.windowsWithValue(key as string, value), 1336 + windowsInSpace: (spaceId: unknown) => 1337 + contextStrict.windowsInSpace(spaceId as string), 1388 1338 1389 1339 // ── Convenience aliases ────────────────────────────────────────── 1390 1340 // These wrap the generic get/set/subscribe surface for the common ··· 1394 1344 setMode: (mode: unknown, options?: { metadata?: Record<string, unknown>; windowId?: number | null }) => { 1395 1345 const metadata = options?.metadata; 1396 1346 const windowId = (options?.windowId ?? null) as number | null; 1397 - return hasContextCapability() 1398 - ? contextStrict.set('mode', mode, metadata, windowId) 1399 - : contextCompat.set('mode', mode, metadata, windowId); 1347 + return contextStrict.set('mode', mode, metadata, windowId); 1400 1348 }, 1401 1349 1402 - getMode: () => hasContextCapability() 1403 - ? contextStrict.get('mode', null) 1404 - : contextCompat.get('mode', null), 1350 + getMode: () => contextStrict.get('mode', null), 1405 1351 1406 1352 /** 1407 1353 * Watch for mode changes. ··· 1418 1364 } 1419 1365 }); 1420 1366 // Fire initial value so caller doesn't have to do a separate getMode(). 1421 - const getPromise = hasContextCapability() 1422 - ? contextStrict.get('mode', null) 1423 - : contextCompat.get('mode', null); 1367 + const getPromise = contextStrict.get('mode', null); 1424 1368 (getPromise as Promise<unknown>).then((result: unknown) => { 1425 1369 const r = result as { success?: boolean; data?: unknown; value?: unknown } | null; 1426 1370 const value = r && typeof r === 'object' ? (('data' in r) ? (r as { data: unknown }).data : (r as { value: unknown }).value) : null; ··· 1431 1375 return unsubscribe; 1432 1376 }, 1433 1377 1434 - getWindowsInSpace: (spaceId: unknown) => hasContextCapability() 1435 - ? contextStrict.windowsInSpace(spaceId as string) 1436 - : contextCompat.windowsInSpace(spaceId as string), 1378 + getWindowsInSpace: (spaceId: unknown) => 1379 + contextStrict.windowsInSpace(spaceId as string), 1437 1380 }; 1438 1381 1439 1382 // ── Dialogs ───────────────────────────────────────────────────────