[eric] browser: Chrome-style suspend capsules (sessionStorage+scroll survive), last-visible-frame snapshots, 20s working grace

This commit is contained in:
ciregenz
2026-07-07 22:52:28 -07:00
parent f7b4e91dd2
commit 161c65cdcd
6 changed files with 168 additions and 5 deletions
+25
View File
@@ -2799,6 +2799,31 @@ async function readPartitionCookies(domain) {
}
ipcMain.handle('get-partition-cookies', (_e, domain) => readPartitionCookies(domain));
// Suspend/resume state capsules: the app renderer stages a resumed webview's sessionStorage snapshot here (keyed by that guest's webContents id) right before loadURL; the guest preload sync-takes it at document-start with an origin match, so page scripts see restored state and logins survive suspension. In-memory only, single-shot, short TTL; a guest can only ever take its OWN capsule.
const pendingSessionCapsules = new Map();
const SESSION_CAPSULE_TTL_MS = 2 * 60 * 1000;
ipcMain.on('browser-capsule-set', (event, wcId, capsule) => {
// Only the app window may stage capsules; a compromised guest must not be able to seed storage into another guest.
if (!mainWindow || event.sender !== mainWindow.webContents) return;
if (typeof wcId !== 'number' || !capsule || typeof capsule.origin !== 'string' || typeof capsule.ss !== 'object') return;
pendingSessionCapsules.set(wcId, { capsule, expiresAt: Date.now() + SESSION_CAPSULE_TTL_MS });
});
ipcMain.on('browser-capsule-take', (event, origin) => {
const entry = pendingSessionCapsules.get(event.sender.id);
if (!entry || entry.expiresAt < Date.now()) {
if (entry) pendingSessionCapsules.delete(event.sender.id);
event.returnValue = null;
return;
}
// Origin-gated take: about:blank and cross-origin redirects leave the capsule staged for the real page (until TTL).
if (entry.capsule.origin !== origin) {
event.returnValue = null;
return;
}
pendingSessionCapsules.delete(event.sender.id);
event.returnValue = entry.capsule;
});
// The renderer relays cookie reads for the session-borrow bridge, but macOS throttles it when the
// window is backgrounded, so those reads intermittently time out. Main never throttles: hold our own
// socket to the backend and answer get_session_cookies here. Cookie reads only; the renderer still
+2
View File
@@ -62,6 +62,8 @@ contextBridge.exposeInMainWorld('openswarm', {
connectSlack: () => ipcRenderer.invoke('connect-slack'),
// Hands a vetted social platform's partition cookies to its session-backed MCP shim (allowlisted domains only, gated again in the main process).
getPartitionCookies: (domain) => ipcRenderer.invoke('get-partition-cookies', domain),
// Suspend/resume state capsule: stages a resumed webview's sessionStorage snapshot in main (keyed by webContents id, short TTL) so the guest preload can sync-take it at document-start. Fire-and-forget; main validates the sender.
setSessionCapsule: (wcId, capsule) => ipcRenderer.send('browser-capsule-set', wcId, capsule),
sendCdpCommand: (wcId, method, params, sessionId) => ipcRenderer.invoke('send-cdp-command', wcId, method, params, sessionId),
cdpDetachClean: (wcId) => ipcRenderer.invoke('cdp-detach-clean', wcId),
cdpCacheSet: (wcId, indexMap) => ipcRenderer.invoke('cdp-cache-set', wcId, indexMap),
+17
View File
@@ -10,6 +10,23 @@
// this webview. Surfaces via main.js's console-message listener.
try { console.warn('[openswarm:webview-preload] loaded for', window.location.href); } catch (_) {}
// Chrome-style suspend/resume: sync-take a pending sessionStorage capsule (staged by the host right before this load) at document-start, so page scripts wake to their old state and logins survive suspension. Origin-matched in main; null for every ordinary navigation.
(function restoreSessionCapsule() {
try {
const { ipcRenderer } = require('electron');
const cap = ipcRenderer.sendSync('browser-capsule-take', window.location.origin);
if (!cap || !cap.ss) return;
for (const k of Object.keys(cap.ss)) {
try { sessionStorage.setItem(k, cap.ss[k]); } catch (_) {}
}
if (cap.sx || cap.sy) {
window.addEventListener('load', () => {
setTimeout(() => { try { window.scrollTo(cap.sx, cap.sy); } catch (_) {} }, 80);
});
}
} catch (_) {}
})();
// Hide webdriver flag
Object.defineProperty(navigator, 'webdriver', {
get: () => false,