experiments in a post-browser web
10
fork

Configure Feed

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

refactor(nav,session): tile:* shims + migrate app/index.js + delete legacy (Phase 3.11b-nav-session)

+232 -130
+1
.yarn/releases
··· 1 + /Users/dietrich/misc/mpeek/.yarn/releases
+7 -7
app/index.js
··· 486 486 name: 'save session', 487 487 description: 'Save current session snapshot', 488 488 execute: async () => { 489 - const result = await api.invoke('session-save'); 489 + const result = await api.session.save(); 490 490 if (result && result.success) { 491 491 return { output: result.message, mimeType: 'text/plain' }; 492 492 } ··· 498 498 name: 'restore previous session', 499 499 description: 'Restore windows from the last saved session', 500 500 execute: async () => { 501 - const result = await api.invoke('session-restore-interactive'); 501 + const result = await api.session.restoreInteractive(); 502 502 if (result && result.success) { 503 503 return { output: result.message, mimeType: 'text/plain' }; 504 504 } ··· 525 525 api.commands.register({ 526 526 name: 'back', 527 527 description: 'Go back in web page history', 528 - execute: () => api.invoke('web-nav-back') 528 + execute: () => api.nav.back() 529 529 }); 530 530 531 531 api.commands.register({ 532 532 name: 'forward', 533 533 description: 'Go forward in web page history', 534 - execute: () => api.invoke('web-nav-forward') 534 + execute: () => api.nav.forward() 535 535 }); 536 536 537 537 api.commands.register({ 538 538 name: 'reload', 539 539 description: 'Reload the current web page', 540 - execute: () => api.invoke('web-nav-reload') 540 + execute: () => api.nav.reload() 541 541 }); 542 542 543 543 api.commands.register({ 544 544 name: 'copy url', 545 545 description: 'Copy the current web page URL to clipboard', 546 546 execute: async () => { 547 - const result = await api.invoke('web-nav-state'); 547 + const result = await api.nav.state(); 548 548 if (result && result.success && result.data && result.data.url) { 549 549 await navigator.clipboard.writeText(result.data.url); 550 550 } ··· 681 681 682 682 // Register "reopen closed window" command for the command palette 683 683 api.subscribe('cmd:execute:reopen closed window', async (msg) => { 684 - const result = await api.invoke('window-reopen-last-closed'); 684 + const result = await api.session.reopenLastClosed(); 685 685 if (msg.expectResult && msg.resultTopic) { 686 686 api.publish(msg.resultTopic, result || { success: true }, api.scopes.GLOBAL); 687 687 }
+5 -122
backend/electron/ipc.ts
··· 146 146 } from './session-partition.js'; 147 147 148 148 import { 149 - saveSessionSnapshot, 150 - restoreSessionSnapshot, 151 - getSessionSnapshotInfo, 152 - } from './session.js'; 153 - 154 - import { 155 149 getIzuiCoordinator, 156 150 } from './izui-state.js'; 157 151 ··· 191 185 // This includes extension windows (peek://{extId}/...) and web pages, 192 186 // but excludes internal background windows and modal windows 193 187 let lastFocusedVisibleWindowId: number | null = null; 188 + 189 + /** Returns the last focused visible window ID, for use by tile-ipc.ts nav shims. */ 190 + export function getLastFocusedVisibleWindowId(): number | null { 191 + return lastFocusedVisibleWindowId; 192 + } 194 193 195 194 /** 196 195 * Update tracking when a content window (http/https) gains focus. ··· 2377 2376 } 2378 2377 }); 2379 2378 2380 - // Reopen last closed window 2381 - ipcMain.handle('window-reopen-last-closed', async () => { 2382 - return reopenLastClosedWindow(); 2383 - }); 2384 - 2385 2379 ipcMain.handle('window-hide', async (_ev, msg) => { 2386 2380 DEBUG && console.log('window-hide', msg); 2387 2381 ··· 2968 2962 const message = error instanceof Error ? error.message : String(error); 2969 2963 return { success: false, error: message }; 2970 2964 } 2971 - }); 2972 - 2973 - // Web navigation handlers - operate on the target http/https window 2974 - const resolveWebWindow = (windowId?: number): BrowserWindow | null => { 2975 - const id = windowId || lastFocusedVisibleWindowId; 2976 - if (!id) return null; 2977 - const win = BrowserWindow.fromId(id); 2978 - if (!win || win.isDestroyed()) return null; 2979 - const winUrl = win.webContents.getURL(); 2980 - if (!winUrl.startsWith('http://') && !winUrl.startsWith('https://')) return null; 2981 - return win; 2982 - }; 2983 - 2984 - ipcMain.handle('web-nav-back', async (_ev, data?: { windowId?: number }) => { 2985 - const win = resolveWebWindow(data?.windowId); 2986 - if (!win) return { success: false, error: 'No web page window' }; 2987 - const nav = win.webContents.navigationHistory; 2988 - if (nav.canGoBack()) { 2989 - nav.goBack(); 2990 - return { success: true }; 2991 - } 2992 - return { success: false, error: 'Cannot go back' }; 2993 - }); 2994 - 2995 - ipcMain.handle('web-nav-forward', async (_ev, data?: { windowId?: number }) => { 2996 - const win = resolveWebWindow(data?.windowId); 2997 - if (!win) return { success: false, error: 'No web page window' }; 2998 - const nav = win.webContents.navigationHistory; 2999 - if (nav.canGoForward()) { 3000 - nav.goForward(); 3001 - return { success: true }; 3002 - } 3003 - return { success: false, error: 'Cannot go forward' }; 3004 - }); 3005 - 3006 - ipcMain.handle('web-nav-reload', async (_ev, data?: { windowId?: number }) => { 3007 - const win = resolveWebWindow(data?.windowId); 3008 - if (!win) return { success: false, error: 'No web page window' }; 3009 - win.webContents.reload(); 3010 - return { success: true }; 3011 - }); 3012 - 3013 - ipcMain.handle('web-nav-state', async (_ev, data?: { windowId?: number }) => { 3014 - const win = resolveWebWindow(data?.windowId); 3015 - if (!win) return { success: false, error: 'No web page window' }; 3016 - const nav = win.webContents.navigationHistory; 3017 - return { 3018 - success: true, 3019 - data: { 3020 - url: win.webContents.getURL(), 3021 - canGoBack: nav.canGoBack(), 3022 - canGoForward: nav.canGoForward(), 3023 - title: win.getTitle() || '', 3024 - } 3025 - }; 3026 2965 }); 3027 2966 3028 2967 // Get last focused visible window ID (for per-window commands) ··· 4038 3977 DEBUG && console.log('[ipc] Web extension handlers registered'); 4039 3978 } 4040 3979 4041 - // ============================================================================ 4042 - // Session Handlers (manual save/restore from renderer) 4043 - // ============================================================================ 4044 - 4045 - function registerSessionHandlers(): void { 4046 - // Manual session save 4047 - ipcMain.handle('session-save', async () => { 4048 - try { 4049 - saveSessionSnapshot('manual'); 4050 - return { success: true, message: 'Session saved' }; 4051 - } catch (error) { 4052 - const message = error instanceof Error ? error.message : String(error); 4053 - return { success: false, error: message }; 4054 - } 4055 - }); 4056 - 4057 - // Interactive session restore — shows confirmation dialog before restoring 4058 - // Skip crash dialog — this handler shows its own confirmation dialog 4059 - ipcMain.handle('session-restore-interactive', async () => { 4060 - try { 4061 - const info = getSessionSnapshotInfo(); 4062 - if (!info) { 4063 - return { success: true, message: 'No saved session to restore', cancelled: false, restored: 0 }; 4064 - } 4065 - 4066 - const { response } = await dialog.showMessageBox({ 4067 - type: 'question', 4068 - title: 'Restore Previous Session', 4069 - message: `Restore ${info.windowCount} window${info.windowCount === 1 ? '' : 's'} from your previous session?\n\nThese will open alongside your current windows.`, 4070 - buttons: ['Restore', 'Cancel'], 4071 - defaultId: 0, 4072 - cancelId: 1, 4073 - }); 4074 - 4075 - if (response !== 0) { 4076 - return { success: true, message: 'Restore cancelled', cancelled: true, restored: 0 }; 4077 - } 4078 - 4079 - const prefs = getPrefs(); 4080 - const result = await restoreSessionSnapshot(prefs, { cleanShutdown: true, crashCount: 0 }); 4081 - return { 4082 - success: true, 4083 - message: `Restored ${result.restored} of ${result.total} window(s)`, 4084 - cancelled: false, 4085 - ...result, 4086 - }; 4087 - } catch (error) { 4088 - const message = error instanceof Error ? error.message : String(error); 4089 - return { success: false, error: message }; 4090 - } 4091 - }); 4092 - 4093 - DEBUG && console.log('[ipc] Session handlers registered'); 4094 - } 4095 - 4096 3980 /** 4097 3981 * Register all IPC handlers 4098 3982 */ ··· 4106 3990 registerProfileHandlers(); 4107 3991 registerModesHandlers(); 4108 3992 registerWebExtensionHandlers(); 4109 - registerSessionHandlers(); 4110 3993 registerMiscHandlers(onQuit); 4111 3994 }
+159 -1
backend/electron/tile-ipc.ts
··· 87 87 import { installFromBundle } from './feature-installer.js'; 88 88 import { resolveCapabilities, validateTileManifest, detectManifestVersion } from './tile-manifest.js'; 89 89 import type { CapabilityGrant, TileCapabilities } from './tile-manifest.js'; 90 - import { invokeWindowOpen, popupToOpener } from './ipc.js'; 90 + import { invokeWindowOpen, popupToOpener, reopenLastClosedWindow, getLastFocusedVisibleWindowId } from './ipc.js'; 91 91 import { 92 92 getRunningExtensions, 93 93 getAllRegisteredExtensions, ··· 121 121 listBackups, 122 122 } from './backup.js'; 123 123 import { getPrefs } from './windows.js'; 124 + import { 125 + saveSessionSnapshot, 126 + restoreSessionSnapshot, 127 + getSessionSnapshotInfo, 128 + } from './session.js'; 124 129 125 130 // Re-export for test access — keeps the public enforcement API on tile-ipc. 126 131 export { validateTileDatastoreRequest } from './tile-datastore-scope.js'; ··· 6625 6630 return { success: false, error: 'trustedBuiltin required for tile:app:get-prefs' }; 6626 6631 } 6627 6632 return getPrefs(); 6633 + }); 6634 + 6635 + // ── tile:nav:* strict shims (Phase 3.11b-nav-session) ──────────────── 6636 + // 6637 + // Strict counterparts of the legacy `web-nav-*` channels in ipc.ts. 6638 + // All require trustedBuiltin — only the core background renderer uses these. 6639 + 6640 + const tileResolveWebWindow = (windowId?: number): BrowserWindow | null => { 6641 + const id = windowId ?? getLastFocusedVisibleWindowId(); 6642 + if (!id) return null; 6643 + const win = BrowserWindow.fromId(id); 6644 + if (!win || win.isDestroyed()) return null; 6645 + const winUrl = win.webContents.getURL(); 6646 + if (!winUrl.startsWith('http://') && !winUrl.startsWith('https://')) return null; 6647 + return win; 6648 + }; 6649 + 6650 + ipcMain.handle('tile:nav:back', (_event, args: { token: string; windowId?: number }) => { 6651 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6652 + const grant = getGrantForToken(args.token); 6653 + if (!grant) return { success: false, error: 'Invalid token' }; 6654 + if (!grant.trustedBuiltin) { 6655 + handleViolation(grant, 'nav', 'tile:nav:back', 'trustedBuiltin required', args.token); 6656 + return { success: false, error: 'trustedBuiltin required for tile:nav:back' }; 6657 + } 6658 + const win = tileResolveWebWindow(args.windowId); 6659 + if (!win) return { success: false, error: 'No web page window' }; 6660 + const nav = win.webContents.navigationHistory; 6661 + if (nav.canGoBack()) { nav.goBack(); return { success: true }; } 6662 + return { success: false, error: 'Cannot go back' }; 6663 + }); 6664 + 6665 + ipcMain.handle('tile:nav:forward', (_event, args: { token: string; windowId?: number }) => { 6666 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6667 + const grant = getGrantForToken(args.token); 6668 + if (!grant) return { success: false, error: 'Invalid token' }; 6669 + if (!grant.trustedBuiltin) { 6670 + handleViolation(grant, 'nav', 'tile:nav:forward', 'trustedBuiltin required', args.token); 6671 + return { success: false, error: 'trustedBuiltin required for tile:nav:forward' }; 6672 + } 6673 + const win = tileResolveWebWindow(args.windowId); 6674 + if (!win) return { success: false, error: 'No web page window' }; 6675 + const nav = win.webContents.navigationHistory; 6676 + if (nav.canGoForward()) { nav.goForward(); return { success: true }; } 6677 + return { success: false, error: 'Cannot go forward' }; 6678 + }); 6679 + 6680 + ipcMain.handle('tile:nav:reload', (_event, args: { token: string; windowId?: number }) => { 6681 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6682 + const grant = getGrantForToken(args.token); 6683 + if (!grant) return { success: false, error: 'Invalid token' }; 6684 + if (!grant.trustedBuiltin) { 6685 + handleViolation(grant, 'nav', 'tile:nav:reload', 'trustedBuiltin required', args.token); 6686 + return { success: false, error: 'trustedBuiltin required for tile:nav:reload' }; 6687 + } 6688 + const win = tileResolveWebWindow(args.windowId); 6689 + if (!win) return { success: false, error: 'No web page window' }; 6690 + win.webContents.reload(); 6691 + return { success: true }; 6692 + }); 6693 + 6694 + ipcMain.handle('tile:nav:state', (_event, args: { token: string; windowId?: number }) => { 6695 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6696 + const grant = getGrantForToken(args.token); 6697 + if (!grant) return { success: false, error: 'Invalid token' }; 6698 + if (!grant.trustedBuiltin) { 6699 + handleViolation(grant, 'nav', 'tile:nav:state', 'trustedBuiltin required', args.token); 6700 + return { success: false, error: 'trustedBuiltin required for tile:nav:state' }; 6701 + } 6702 + const win = tileResolveWebWindow(args.windowId); 6703 + if (!win) return { success: false, error: 'No web page window' }; 6704 + const nav = win.webContents.navigationHistory; 6705 + return { 6706 + success: true, 6707 + data: { 6708 + url: win.webContents.getURL(), 6709 + canGoBack: nav.canGoBack(), 6710 + canGoForward: nav.canGoForward(), 6711 + title: win.getTitle() || '', 6712 + } 6713 + }; 6714 + }); 6715 + 6716 + // ── tile:session:* strict shims (Phase 3.11b-nav-session) ───────────── 6717 + // 6718 + // Strict counterparts of the legacy `session-save`, 6719 + // `session-restore-interactive`, and `window-reopen-last-closed` channels. 6720 + // All require trustedBuiltin. 6721 + 6722 + ipcMain.handle('tile:session:save', async (_event, args: { token: string }) => { 6723 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6724 + const grant = getGrantForToken(args.token); 6725 + if (!grant) return { success: false, error: 'Invalid token' }; 6726 + if (!grant.trustedBuiltin) { 6727 + handleViolation(grant, 'session', 'tile:session:save', 'trustedBuiltin required', args.token); 6728 + return { success: false, error: 'trustedBuiltin required for tile:session:save' }; 6729 + } 6730 + try { 6731 + saveSessionSnapshot('manual'); 6732 + return { success: true, message: 'Session saved' }; 6733 + } catch (error) { 6734 + const message = error instanceof Error ? error.message : String(error); 6735 + return { success: false, error: message }; 6736 + } 6737 + }); 6738 + 6739 + ipcMain.handle('tile:session:restore-interactive', async (_event, args: { token: string }) => { 6740 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6741 + const grant = getGrantForToken(args.token); 6742 + if (!grant) return { success: false, error: 'Invalid token' }; 6743 + if (!grant.trustedBuiltin) { 6744 + handleViolation(grant, 'session', 'tile:session:restore-interactive', 'trustedBuiltin required', args.token); 6745 + return { success: false, error: 'trustedBuiltin required for tile:session:restore-interactive' }; 6746 + } 6747 + try { 6748 + const info = getSessionSnapshotInfo(); 6749 + if (!info) { 6750 + return { success: true, message: 'No saved session to restore', cancelled: false, restored: 0 }; 6751 + } 6752 + const { response } = await dialog.showMessageBox({ 6753 + type: 'question', 6754 + title: 'Restore Previous Session', 6755 + message: `Restore ${info.windowCount} window${info.windowCount === 1 ? '' : 's'} from your previous session?\n\nThese will open alongside your current windows.`, 6756 + buttons: ['Restore', 'Cancel'], 6757 + defaultId: 0, 6758 + cancelId: 1, 6759 + }); 6760 + if (response !== 0) { 6761 + return { success: true, message: 'Restore cancelled', cancelled: true, restored: 0 }; 6762 + } 6763 + const prefs = getPrefs(); 6764 + const result = await restoreSessionSnapshot(prefs, { cleanShutdown: true, crashCount: 0 }); 6765 + return { 6766 + success: true, 6767 + message: `Restored ${result.restored} of ${result.total} window(s)`, 6768 + cancelled: false, 6769 + ...result, 6770 + }; 6771 + } catch (error) { 6772 + const message = error instanceof Error ? error.message : String(error); 6773 + return { success: false, error: message }; 6774 + } 6775 + }); 6776 + 6777 + ipcMain.handle('tile:session:reopen-last-closed', (_event, args: { token: string }) => { 6778 + if (!args?.token) return { success: false, error: 'Invalid token' }; 6779 + const grant = getGrantForToken(args.token); 6780 + if (!grant) return { success: false, error: 'Invalid token' }; 6781 + if (!grant.trustedBuiltin) { 6782 + handleViolation(grant, 'session', 'tile:session:reopen-last-closed', 'trustedBuiltin required', args.token); 6783 + return { success: false, error: 'trustedBuiltin required for tile:session:reopen-last-closed' }; 6784 + } 6785 + return reopenLastClosedWindow(); 6628 6786 }); 6629 6787 6630 6788 DEBUG && console.log('[tile-ipc] All tile IPC handlers registered');
+59
backend/electron/tile-preload.cts
··· 2108 2108 }, 2109 2109 }; 2110 2110 2111 + // ── Nav (trustedBuiltin only) ───────────────────────────────────────── 2112 + // 2113 + // Strict counterparts of the legacy `web-nav-*` channels. Consumed by the 2114 + // core background renderer (app/index.js) for the back/forward/reload/copy-url 2115 + // cmd palette commands (Phase 3.11b-nav-session). 2116 + api.nav = { 2117 + back: (windowId?: number) => { 2118 + if (!trustedBuiltin) { 2119 + return Promise.resolve({ success: false, error: 'api.nav requires trustedBuiltin' }); 2120 + } 2121 + return ipcRenderer.invoke('tile:nav:back', { token: tileToken, windowId }); 2122 + }, 2123 + forward: (windowId?: number) => { 2124 + if (!trustedBuiltin) { 2125 + return Promise.resolve({ success: false, error: 'api.nav requires trustedBuiltin' }); 2126 + } 2127 + return ipcRenderer.invoke('tile:nav:forward', { token: tileToken, windowId }); 2128 + }, 2129 + reload: (windowId?: number) => { 2130 + if (!trustedBuiltin) { 2131 + return Promise.resolve({ success: false, error: 'api.nav requires trustedBuiltin' }); 2132 + } 2133 + return ipcRenderer.invoke('tile:nav:reload', { token: tileToken, windowId }); 2134 + }, 2135 + state: (windowId?: number) => { 2136 + if (!trustedBuiltin) { 2137 + return Promise.resolve({ success: false, error: 'api.nav requires trustedBuiltin' }); 2138 + } 2139 + return ipcRenderer.invoke('tile:nav:state', { token: tileToken, windowId }); 2140 + }, 2141 + }; 2142 + 2143 + // ── Session (trustedBuiltin only) ───────────────────────────────────── 2144 + // 2145 + // Strict counterparts of the legacy `session-save`, 2146 + // `session-restore-interactive`, and `window-reopen-last-closed` channels. 2147 + // Consumed by the core background renderer (app/index.js) 2148 + // (Phase 3.11b-nav-session). 2149 + api.session = { 2150 + save: () => { 2151 + if (!trustedBuiltin) { 2152 + return Promise.resolve({ success: false, error: 'api.session requires trustedBuiltin' }); 2153 + } 2154 + return ipcRenderer.invoke('tile:session:save', { token: tileToken }); 2155 + }, 2156 + restoreInteractive: () => { 2157 + if (!trustedBuiltin) { 2158 + return Promise.resolve({ success: false, error: 'api.session requires trustedBuiltin' }); 2159 + } 2160 + return ipcRenderer.invoke('tile:session:restore-interactive', { token: tileToken }); 2161 + }, 2162 + reopenLastClosed: () => { 2163 + if (!trustedBuiltin) { 2164 + return Promise.resolve({ success: false, error: 'api.session requires trustedBuiltin' }); 2165 + } 2166 + return ipcRenderer.invoke('tile:session:reopen-last-closed', { token: tileToken }); 2167 + }, 2168 + }; 2169 + 2111 2170 return api; 2112 2171 } 2113 2172
+1
node_modules
··· 1 + /Users/dietrich/misc/mpeek/node_modules