experiments in a post-browser web
10
fork

Configure Feed

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

feat(izui): implement role-based window policy (Phase 1-2)

Phase 1: Role inference in window-open handler. Computes role from
existing params (overlay->overlay, modal+keepLive->palette,
modal->quick-view, escapeMode:navigate->workspace, etc.)

Phase 2: Rewrote handleEscapeForWindow as policy table lookup.
~80 lines of nested if/else replaced with escPolicy(sessionState, role)
pure function + ~25 line handler. All 105 tests pass.

+59 -86
+25 -1
backend/electron/ipc.ts
··· 1845 1845 // Add to window manager with modal parameter 1846 1846 // Note: isTransient was determined BEFORE window creation (at top of handler) 1847 1847 // to avoid the race condition where the new window becomes focused before we check 1848 + const parentWindowId = isRealParent ? openerWindow!.id : null; 1849 + 1850 + // Phase 1: Compute IZUI role from existing params if not explicitly provided 1851 + let role = options.role as string | undefined; 1852 + if (!role) { 1853 + if (options.overlay === true) { 1854 + role = 'overlay'; 1855 + } else if (options.modal === true && options.keepLive === true) { 1856 + role = 'palette'; 1857 + } else if (options.modal === true) { 1858 + role = 'quick-view'; 1859 + } else if (options.escapeMode === 'navigate') { 1860 + role = 'workspace'; 1861 + } else if (parentWindowId != null && isWebPage) { 1862 + role = 'child-content'; 1863 + } else if (isWebPage) { 1864 + role = 'content'; 1865 + } else { 1866 + role = 'utility'; 1867 + } 1868 + } 1869 + console.log('[izui] Window role:', role, 'for:', url); 1870 + 1848 1871 const windowParams = { 1849 1872 ...options, 1850 1873 address: url, 1851 1874 transient: isTransient, 1852 - parentWindowId: isRealParent ? openerWindow!.id : null, 1875 + parentWindowId, 1876 + role, 1853 1877 }; 1854 1878 console.log('Adding window to manager:', win.id, 'escapeMode:', windowParams.escapeMode, 'modal:', windowParams.modal, 'keepLive:', windowParams.keepLive, 'transient:', isTransient); 1855 1879 registerWindow(win.id, msg.source, windowParams);
+34 -85
backend/electron/windows.ts
··· 100 100 } 101 101 102 102 /** 103 + * ESC policy: given session state and window role, determine what to do. 104 + * Pure function — no side effects. 105 + */ 106 + function escPolicy(sessionState: string, role: string): 'close' | 'close-and-restore' | 'nothing' { 107 + // These roles always close regardless of session state 108 + if (['quick-view', 'palette', 'utility'].includes(role)) return 'close'; 109 + if (role === 'overlay') return 'close-and-restore'; 110 + if (role === 'child-content') return 'close'; 111 + // workspace and content: only close in transient sessions 112 + if (sessionState === 'transient') return 'close'; 113 + return 'nothing'; 114 + } 115 + 116 + /** 103 117 * Core ESC handling logic for a BrowserWindow. 104 118 * Called when ESC keyDown is detected on the window's own webContents 105 119 * or on a webview guest webContents within the window. 106 120 * 121 + * Uses IZUI window roles to determine behavior via escPolicy(). 122 + * 107 123 * @param bw - The BrowserWindow to handle ESC for 108 124 * @param source - 'host' if from the window's own webContents, 'webview' if from a guest 109 125 */ 110 126 async function handleEscapeForWindow(bw: BrowserWindow, source: 'host' | 'webview'): Promise<void> { 111 - // Get window info 112 127 const entry = getWindowInfo(bw.id); 113 - const params = entry?.params || {}; 114 - const escapeMode = (params.escapeMode as string) || 'auto'; 128 + if (!entry || bw.isDestroyed()) return; 115 129 116 - // Always log ESC handling for debugging 117 - console.log(`[esc] ESC keyDown - window ${bw.id}, source: ${source}, escapeMode: ${escapeMode}, transient: ${params.transient}, url: ${params.address}`); 130 + const params = entry.params; 131 + const role = (params.role as string) || 'utility'; 132 + console.log(`[esc] ESC keyDown - window ${bw.id}, source: ${source}, role: ${role}, url: ${params.address}`); 118 133 119 - // For 'ignore' mode, do nothing - let ESC pass through 120 - if (escapeMode === 'ignore') { 121 - DEBUG && console.log('Ignore mode - not handling ESC'); 134 + // escapeMode: 'ignore' is a hard override — let ESC pass through entirely 135 + if (params.escapeMode === 'ignore') { 136 + DEBUG && console.log('[esc] Ignore mode - not handling ESC'); 122 137 return; 123 138 } 124 139 125 - // For 'navigate' mode, always ask renderer first. 126 - // If renderer handles internally, do nothing. 127 - // If renderer returns { handled: false }, backend applies IZUI close policy: 128 - // - Child windows (have parentWindowId) close to return to parent 129 - // - Transient windows close 130 - // - Active root windows do NOT close (IZUI policy) 131 - if (escapeMode === 'navigate') { 132 - console.log(`[esc] navigate mode - forwarding to renderer`); 133 - const response = await askRendererToHandleEscape(bw); 134 - console.log(`[esc] navigate mode response:`, response); 135 - if (response.handled) { 136 - return; 137 - } 138 - // Renderer didn't handle - apply IZUI close policy 139 - const coordinator = getIzuiCoordinator(); 140 - const isChild = !!params.parentWindowId; 141 - const isTransient = coordinator.isTransient() || params.transient === true; 142 - if (isChild || isTransient || response.close) { 143 - console.log(`[esc] navigate mode - closing (isChild: ${isChild}, isTransient: ${isTransient}, close: ${response.close})`); 144 - closeOrHideWindow(bw.id); 145 - } else { 146 - console.log('[esc] navigate mode - active root window, NOT closing (IZUI policy)'); 147 - } 148 - return; 149 - } 140 + // Ask renderer first (dialog close, internal navigation, etc.) 141 + const response = await askRendererToHandleEscape(bw); 142 + if (response.handled) return; 150 143 151 - // For 'auto' mode, check if transient (no focused window when opened) 152 - if (escapeMode === 'auto') { 153 - // Use IZUI coordinator for consistent state management 154 - const coordinator = getIzuiCoordinator(); 155 - const isCoordinatorOverlay = coordinator.isOverlay(); 156 - 157 - // Overlay windows always close on ESC (they're full-screen temporary views) 158 - const isOverlay = isCoordinatorOverlay || params.overlay === true || params.overlayHiddenWindows; 159 - 160 - // Child windows (have a parentWindowId) always close on ESC at root 161 - // This replaces the client-side parentWindowId check in izui.js 162 - const isChild = !!params.parentWindowId; 163 - 164 - // Check transient from coordinator (re-evaluated) or window params (fallback) 165 - const isTransient = coordinator.isTransient() || params.transient === true; 144 + // Look up session state and apply role-based policy 145 + const coordinator = getIzuiCoordinator(); 146 + const sessionState = coordinator.getState(); 147 + const action = escPolicy(sessionState, role); 148 + console.log(`[esc] escPolicy(${sessionState}, ${role}) -> ${action}`); 166 149 167 - if (isChild || isTransient || isOverlay) { 168 - // Child window, transient mode, or overlay - ask renderer first, then close 169 - if (isChild && !isTransient && !isOverlay) { 170 - // Child in active mode: ask renderer for internal nav first 171 - console.log(`[esc] auto mode (child window) - asking renderer before closing`); 172 - const response = await askRendererToHandleEscape(bw); 173 - console.log(`[esc] Child window escape response:`, response); 174 - if (response.handled) { 175 - return; 176 - } 177 - // Not handled internally - close the child window 178 - console.log('[esc] Child window at root - closing to return to parent'); 179 - } else { 180 - // Transient mode or overlay - close immediately 181 - console.log('[esc] Auto mode (transient/overlay/transient-child) - closing directly, isChild:', isChild, 'coordinator.isTransient:', coordinator.isTransient()); 182 - } 183 - } else { 184 - // Active mode, root window (no parent) - ESC only navigates internal state, does NOT close 185 - // Unless renderer explicitly requests close via { close: true } 186 - console.log(`[esc] auto mode (non-transient/active, root window) - asking renderer to handle`); 187 - const response = await askRendererToHandleEscape(bw); 188 - console.log(`[esc] Renderer escape response (auto/active):`, response); 189 - if (response.handled) { 190 - return; 191 - } 192 - if (response.close) { 193 - console.log('[esc] Active mode - renderer explicitly requested close'); 194 - closeOrHideWindow(bw.id); 195 - return; 196 - } 197 - console.log('[esc] Active mode (root window) - renderer did not handle, NOT closing (IZUI policy)'); 198 - return; 199 - } 150 + if (action === 'close' || action === 'close-and-restore') { 151 + closeOrHideWindow(bw.id); 200 152 } 201 - 202 - // Only 'close' mode and transient 'auto' mode reach here 203 - console.log('[esc] Closing/hiding window via closeOrHideWindow'); 204 - closeOrHideWindow(bw.id); 153 + // 'nothing' -> return silently (active workspace/content at root) 205 154 } 206 155 207 156 /**