experiments in a post-browser web
10
fork

Configure Feed

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

fix(shortcuts): cmd+shift+T (reopen closed window) works from inside webview content

User-reported: cmd+shift+T "doesn't work a lot of the time".

Root cause: when the user's focus is inside a page's webview content
(the actual webpage they're reading, not the page chrome), keystrokes
fire `before-input-event` on the GUEST webContents - not the host
window's webContents. The page tile's guest handler in `ipc.ts:1471`
(canvas) and `:1271` (popup) was a hardcoded list of page-specific
shortcuts (cmd+L/R/F/G/[/]) and never delegated unmatched shortcuts
to the global `handleLocalShortcut`. So cmd+shift+T - and any other
top-level local shortcut, e.g. cmd+N - silently did nothing whenever
focus was inside the page content. Which is most of the time.

Fix: added an `else if (handleLocalShortcut(input, win.id))` fallback
to both webview guest before-input-event handlers. The host-level
handler at `main.ts:570` already delegates correctly; the guests now
match.

Test: `tests/desktop/reopen-closed-window.spec.ts` - 4 cases:
- basic open/close/reopen single round trip
- rapid 5-iteration open/close/reopen loop (catches the
fire-and-forget pubsub race if it ever surfaces)
- cmd+shift+T sent into the webview guest's webContents (the
user-reported repro - this test was the failing baseline)
- keepLive workspace tile hide-vs-close stack invariant

Exposed `reopenLastClosedWindow`, `getClosedWindowStack`,
`getClosedWindowCount` on `__peek_test` for the tests.

All other shortcut-affected suites still green: shortcut-roundtrip
(1/1), page-navbar (4/4 incl. Cmd+L), page-redirect (4/4),
page-load-failure (4/4).

+329 -3
+9 -1
backend/electron/entry.ts
··· 70 70 loadFeatureManifest, 71 71 // Closed window stack persistence 72 72 loadClosedWindowStack, 73 + getClosedWindowStack, 74 + getClosedWindowCount, 73 75 } from './index.js'; 74 76 75 77 import { startHotReload, stopHotReload } from './hotreload.js'; ··· 128 130 const DEBUG = !!process.env.DEBUG; 129 131 130 132 // Expose test utilities on global for Playwright evaluateMain access 131 - (globalThis as any).__peek_test = { handleLocalShortcut, handleExternalUrl }; 133 + (globalThis as any).__peek_test = { 134 + handleLocalShortcut, 135 + handleExternalUrl, 136 + reopenLastClosedWindow, 137 + getClosedWindowStack, 138 + getClosedWindowCount, 139 + }; 132 140 (globalThis as any).__peek_electron = { globalShortcut }; 133 141 134 142 // Parse --load-extension CLI arguments for dev workflow.
+2
backend/electron/index.ts
··· 175 175 validateThemeCSS, 176 176 loadClosedWindowStack, 177 177 saveClosedWindowStack, 178 + getClosedWindowStack, 179 + getClosedWindowCount, 178 180 } from './main.js'; 179 181 180 182 export type { AppConfig } from './main.js';
+16 -2
backend/electron/ipc.ts
··· 87 87 getSystemAddress, 88 88 } from './pubsub.js'; 89 89 90 + import { handleLocalShortcut } from './shortcuts.js'; 91 + 90 92 import { 91 93 getSyncConfig, 92 94 syncAll, ··· 1267 1269 console.log('[webview-popup] Failed to register download handler on guest session:', e); 1268 1270 } 1269 1271 1270 - // Keyboard shortcuts inside the webview guest for page navigation 1272 + // Keyboard shortcuts inside the webview guest for page navigation. 1273 + // Page-specific shortcuts (cmd+L, cmd+R, etc.) are handled inline. 1274 + // Anything else falls through to handleLocalShortcut so global 1275 + // shortcuts (cmd+shift+T undo-close, cmd+N new window) work even 1276 + // when the user's focus is inside the webpage they're reading. 1271 1277 guestWebContents.on('before-input-event', (event, input) => { 1272 1278 const modifier = process.platform === 'darwin' ? input.meta : input.control; 1273 1279 if (input.type !== 'keyDown' || !modifier) return; ··· 1291 1297 event.preventDefault(); 1292 1298 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 1293 1299 publish('peek://system/', 'page:go-forward', { windowId: popupWin.id }); 1300 + event.preventDefault(); 1301 + } else if (handleLocalShortcut(input, popupWin.id)) { 1294 1302 event.preventDefault(); 1295 1303 } 1296 1304 }); ··· 1467 1475 console.log('[webview-popup] Failed to register download handler on guest session:', e); 1468 1476 } 1469 1477 1470 - // Keyboard shortcuts inside the webview guest for page navigation 1478 + // Keyboard shortcuts inside the webview guest for page navigation. 1479 + // Page-specific shortcuts (cmd+L, cmd+R, etc.) are handled inline. 1480 + // Anything else falls through to handleLocalShortcut so global 1481 + // shortcuts (cmd+shift+T undo-close, cmd+N new window) work even 1482 + // when the user's focus is inside the webpage they're reading. 1471 1483 guestWebContents.on('before-input-event', (event, input) => { 1472 1484 const modifier = process.platform === 'darwin' ? input.meta : input.control; 1473 1485 if (input.type !== 'keyDown' || !modifier) return; ··· 1491 1503 event.preventDefault(); 1492 1504 } else if (input.key === ']' || (input.key === 'ArrowRight' && !input.shift)) { 1493 1505 publish('peek://system/', 'page:go-forward', { windowId: win.id }); 1506 + event.preventDefault(); 1507 + } else if (handleLocalShortcut(input, win.id)) { 1494 1508 event.preventDefault(); 1495 1509 } 1496 1510 });
+302
tests/desktop/reopen-closed-window.spec.ts
··· 1 + /** 2 + * Reopen Last Closed Window (cmd+shift+t) Tests 3 + * 4 + * Repro for user-reported flakiness: cmd+shift+t "doesn't work a lot of 5 + * the time". Hypothesis from architecture audit: 6 + * reopenLastClosedWindow() pops from the in-memory stack and publishes 7 + * `window:reopen-request` to the bgWindow via fire-and-forget pubsub. 8 + * If the bgWindow's createWindow call throws/races, the entry is gone 9 + * from the stack and no window comes back. 10 + * 11 + * These tests open + close + reopen N times in a tight loop to surface 12 + * the flakiness deterministically. 13 + * 14 + * Run with: 15 + * yarn test:grep "Reopen Last Closed" 16 + */ 17 + 18 + import { test, expect, DesktopApp } from '../fixtures/desktop-app'; 19 + import { Page } from '@playwright/test'; 20 + import { createPerDescribeApp } from '../helpers/test-app'; 21 + import { sleep } from '../helpers/window-utils'; 22 + import http from 'http'; 23 + 24 + let app: DesktopApp; 25 + let bgWindow: Page; 26 + let server: http.Server; 27 + let serverPort: number; 28 + 29 + test.describe('Reopen Last Closed Window @desktop', () => { 30 + test.beforeAll(async () => { 31 + ({ app, bgWindow } = await createPerDescribeApp('reopen-closed')); 32 + 33 + await new Promise<void>((resolve) => { 34 + server = http.createServer((req, res) => { 35 + const path = req.url || '/'; 36 + res.writeHead(200, { 'Content-Type': 'text/html' }); 37 + res.end( 38 + `<!DOCTYPE html><html><head><title>${path}</title></head>` + 39 + `<body><h1 id="page">${path}</h1></body></html>`, 40 + ); 41 + }); 42 + server.listen(0, '127.0.0.1', () => { 43 + const addr = server.address(); 44 + serverPort = typeof addr === 'object' && addr ? addr.port : 0; 45 + resolve(); 46 + }); 47 + }); 48 + }); 49 + 50 + test.afterAll(async () => { 51 + if (server) server.close(); 52 + if (app) await app.close(); 53 + }); 54 + 55 + // Helpers -------------------------------------------------------------- 56 + 57 + async function openWindow(url: string): Promise<number> { 58 + const result = await bgWindow.evaluate(async (u: string) => { 59 + return await (window as any).app.window.open(u, { width: 700, height: 500 }); 60 + }, url); 61 + expect(result.success).toBe(true); 62 + return result.id; 63 + } 64 + 65 + async function closeWindowById(windowId: number): Promise<void> { 66 + await bgWindow.evaluate(async (id: number) => { 67 + return await (window as any).app.window.close(id); 68 + }, windowId); 69 + } 70 + 71 + async function getStackCount(): Promise<number> { 72 + const count = await app.evaluateMain!(() => { 73 + return (globalThis as any).__peek_test.getClosedWindowCount(); 74 + }); 75 + return count as number; 76 + } 77 + 78 + async function reopenViaShortcut(): Promise<{ success: boolean; url?: string; error?: string }> { 79 + return (await app.evaluateMain!(() => { 80 + return (globalThis as any).__peek_test.reopenLastClosedWindow(); 81 + })) as any; 82 + } 83 + 84 + async function findWindowWithUrl(urlSubstr: string, timeoutMs = 5000): Promise<Page | null> { 85 + const deadline = Date.now() + timeoutMs; 86 + while (Date.now() < deadline) { 87 + for (const w of app.windows()) { 88 + if (w.url().includes(urlSubstr)) return w; 89 + } 90 + await sleep(100); 91 + } 92 + return null; 93 + } 94 + 95 + // Tests ---------------------------------------------------------------- 96 + 97 + test('open → close → reopen brings the URL back (single round trip)', async () => { 98 + const url = `http://127.0.0.1:${serverPort}/single`; 99 + 100 + const id = await openWindow(url); 101 + await findWindowWithUrl('/single'); 102 + 103 + const stackBefore = await getStackCount(); 104 + await closeWindowById(id); 105 + 106 + // Wait for 'closed' event handler to push to stack 107 + await bgWindow.waitForFunction( 108 + async (expected: number) => { 109 + // poll the count via a no-op api call to ensure we don't bypass the lifecycle 110 + return true; // placeholder; real check is below via getStackCount 111 + }, 112 + stackBefore + 1, 113 + { timeout: 3000 }, 114 + ); 115 + // Real check 116 + const stackAfter = await getStackCount(); 117 + expect(stackAfter).toBe(stackBefore + 1); 118 + 119 + const result = await reopenViaShortcut(); 120 + expect(result.success).toBe(true); 121 + expect(result.url).toContain('/single'); 122 + 123 + const reopened = await findWindowWithUrl('/single', 8000); 124 + expect(reopened).toBeTruthy(); 125 + }); 126 + 127 + test('rapid open/close/reopen — 5 iterations, each must come back', async () => { 128 + // Tight loop to surface the fire-and-forget pubsub race. If the 129 + // background window misses any `window:reopen-request` event, an 130 + // entry will be popped from the stack but no window will appear. 131 + const failures: string[] = []; 132 + 133 + for (let i = 0; i < 5; i++) { 134 + const tag = `iter-${i}-${Date.now()}`; 135 + const url = `http://127.0.0.1:${serverPort}/${tag}`; 136 + 137 + const id = await openWindow(url); 138 + const opened = await findWindowWithUrl(tag, 5000); 139 + if (!opened) { 140 + failures.push(`${tag}: window did not open`); 141 + continue; 142 + } 143 + 144 + await closeWindowById(id); 145 + // Give the 'closed' handler a moment to push. 146 + await sleep(150); 147 + 148 + const stackCount = await getStackCount(); 149 + if (stackCount === 0) { 150 + failures.push(`${tag}: closed window not on stack (count=0)`); 151 + continue; 152 + } 153 + 154 + const result = await reopenViaShortcut(); 155 + if (!result.success) { 156 + failures.push(`${tag}: reopen returned !success: ${result.error}`); 157 + continue; 158 + } 159 + 160 + const reopened = await findWindowWithUrl(tag, 8000); 161 + if (!reopened) { 162 + failures.push(`${tag}: reopen reported success but no window appeared`); 163 + continue; 164 + } 165 + 166 + // Cleanup the reopened window for the next iteration. Pop it off 167 + // by id so it doesn't pollute the stack for the next reopen. 168 + try { 169 + const reopenedId = await bgWindow.evaluate(async (u: string) => { 170 + const r = await (window as any).app.window.list({ includeInternal: false }); 171 + if (!r.success) return null; 172 + const win = r.windows.find((w: any) => w.url?.includes(u)); 173 + return win?.id ?? null; 174 + }, tag); 175 + if (reopenedId != null) await closeWindowById(reopenedId); 176 + await sleep(100); 177 + } catch {} 178 + } 179 + 180 + expect(failures).toEqual([]); 181 + }); 182 + 183 + test('cmd+shift+T from inside webview content triggers reopen (regression: guest webContents must delegate to handleLocalShortcut)', async () => { 184 + // The user-reported "doesn't work a lot of the time" symptom: when 185 + // focus is inside the page content (the webview guest), pressing 186 + // cmd+shift+T silently does nothing. The webview guest's 187 + // `before-input-event` handler in `ipc.ts` only handles 188 + // page-specific shortcuts (cmd+L, R, F, …) and never delegates 189 + // unmatched keys to the global `handleLocalShortcut`. 190 + 191 + // Stack a closeable URL so the reopen has something to bring back. 192 + const reopenableUrl = `http://127.0.0.1:${serverPort}/from-webview-shortcut`; 193 + const id1 = await openWindow(reopenableUrl); 194 + await findWindowWithUrl('/from-webview-shortcut'); 195 + await closeWindowById(id1); 196 + await sleep(200); 197 + expect(await getStackCount()).toBeGreaterThan(0); 198 + 199 + // Open a SECOND page with its own webview guest. Focus will start 200 + // inside this guest content (Peek's page tile auto-focuses the 201 + // webview after dom-ready). 202 + const activeUrl = `http://127.0.0.1:${serverPort}/active-page`; 203 + const id2 = await openWindow(activeUrl); 204 + await findWindowWithUrl('/active-page'); 205 + // Give the page tile time to attach the webview + auto-focus it. 206 + await sleep(500); 207 + 208 + // Synthesize cmd+shift+T into the GUEST webContents — i.e. simulate 209 + // pressing the shortcut while focus is inside the webpage the user 210 + // is reading. This is what triggers the page-host's webview 211 + // before-input-event listener. 212 + const result = (await app.evaluateMain!(({ webContents }) => { 213 + const all = webContents.getAllWebContents(); 214 + const guests = all.filter((wc: any) => typeof wc.hostWebContents !== 'undefined' && wc.hostWebContents); 215 + if (guests.length === 0) return { error: 'no guest webContents found', total: all.length }; 216 + // Pick the most recently created guest — the active-page webview. 217 + const guest = guests[guests.length - 1]; 218 + try { 219 + guest.sendInputEvent({ 220 + type: 'keyDown', 221 + keyCode: 'T', 222 + modifiers: process.platform === 'darwin' ? ['meta', 'shift'] : ['control', 'shift'], 223 + } as any); 224 + return { ok: true, url: guest.getURL() }; 225 + } catch (e: any) { 226 + return { error: e.message }; 227 + } 228 + })) as any; 229 + 230 + expect(result.error, JSON.stringify(result)).toBeUndefined(); 231 + 232 + const reopened = await findWindowWithUrl('/from-webview-shortcut', 8000); 233 + expect(reopened, 'cmd+shift+T from inside the webview must reopen the closed window').toBeTruthy(); 234 + 235 + // Cleanup 236 + await closeWindowById(id2); 237 + if (reopened) { 238 + const reopenedId = await bgWindow.evaluate(async (u: string) => { 239 + const r = await (window as any).app.window.list({ includeInternal: false }); 240 + if (!r.success) return null; 241 + const w = r.windows.find((win: any) => win.url?.includes(u)); 242 + return w?.id ?? null; 243 + }, '/from-webview-shortcut'); 244 + if (reopenedId != null) await closeWindowById(reopenedId); 245 + } 246 + }); 247 + 248 + test('hide-then-close keepLive tile: stack does NOT lose the most recent real close', async () => { 249 + // A workspace tile that is `keepLive: true` is hidden (not closed) 250 + // when you cmd+W it. If the most-recently-pushed entry was a real 251 + // page close before that, undo-close should still bring back the page. 252 + // Also covers the inverse: closing a non-reopenable window (HUD, 253 + // background) shouldn't break the stack for a subsequent real close. 254 + 255 + const pageUrl = `http://127.0.0.1:${serverPort}/before-keep-live`; 256 + const pageId = await openWindow(pageUrl); 257 + await findWindowWithUrl('/before-keep-live'); 258 + 259 + // Now open and close a websearch home tile (keepLive workspace). 260 + // Closing it should hide, not push to stack. 261 + await bgWindow.evaluate(async () => { 262 + const api = (window as any).app; 263 + api.publish('cmd:execute:open web search', {}); 264 + }); 265 + 266 + // Wait for the websearch window to appear, then close it. 267 + let websearchId: number | null = null; 268 + const deadline = Date.now() + 10_000; 269 + while (Date.now() < deadline) { 270 + const result = await bgWindow.evaluate(async () => { 271 + const r = await (window as any).app.window.list({ includeInternal: true }); 272 + if (!r.success) return null; 273 + const win = r.windows.find((w: any) => (w.url || '').includes('peek://websearch/')); 274 + return win?.id ?? null; 275 + }); 276 + if (result != null) { 277 + websearchId = result; 278 + break; 279 + } 280 + await sleep(150); 281 + } 282 + expect(websearchId).not.toBeNull(); 283 + 284 + const stackBeforeWebsearchClose = await getStackCount(); 285 + await closeWindowById(websearchId!); 286 + await sleep(300); 287 + const stackAfterWebsearchClose = await getStackCount(); 288 + // keepLive tile hides — must NOT push. 289 + expect(stackAfterWebsearchClose).toBe(stackBeforeWebsearchClose); 290 + 291 + // Now close the page tile. 292 + await closeWindowById(pageId); 293 + await sleep(300); 294 + 295 + // Reopen — should bring back the page tile, not anything else. 296 + const result = await reopenViaShortcut(); 297 + expect(result.success).toBe(true); 298 + expect(result.url).toContain('/before-keep-live'); 299 + const reopened = await findWindowWithUrl('/before-keep-live', 8000); 300 + expect(reopened).toBeTruthy(); 301 + }); 302 + });