From 5698be5a1b499789153ccb0a31096f82e24925b9 Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:52:05 +0000 Subject: [PATCH] Cancel the iframe shim's pending leader tap on any other key The shim's dual-tap detector never reset between Shift or Meta taps, so typing a left-Shift capital, a word, then a right-Shift capital inside a proxied iframe posted 'leader' and dropped the pane out of passthrough. The Wall's own detector already resets on any other key. --- lib/src/host/iframe-proxy-rewrite.test.ts | 14 ++++++++++++++ lib/src/host/iframe-proxy-rewrite.ts | 9 +++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/src/host/iframe-proxy-rewrite.test.ts b/lib/src/host/iframe-proxy-rewrite.test.ts index e2d56152c..1f0b3d82b 100644 --- a/lib/src/host/iframe-proxy-rewrite.test.ts +++ b/lib/src/host/iframe-proxy-rewrite.test.ts @@ -105,6 +105,20 @@ describe('instrumentHtml', () => { expect(IFRAME_SHIM).toContain('pushState'); }); + it('posts the leader for a left-then-right tap, and not when another key falls between', () => { + const delivered: unknown[] = []; + const frame = shimFrame(APP, (data) => delivered.push(data)); + const key = (k: string, location = 0) => frame.emit('keydown', { key: k, location }); + key('Shift', 1); key('Shift', 2); + key('Meta', 1); key('Meta', 2); + expect(delivered.filter(d => (d as { __dormouse: string }).__dormouse === 'leader')).toHaveLength(2); + delivered.length = 0; + // "I Am": typing, not the gesture. + key('Shift', 1); key('I'); key(' '); key('Shift', 2); + key('Meta', 1); key('c'); key('Meta', 2); + expect(delivered.filter(d => (d as { __dormouse: string }).__dormouse === 'leader')).toHaveLength(0); + }); + it('intercepts new-tab attempts (target=_blank / window.open) as open-window', () => { expect(IFRAME_SHIM).toContain("'open-window'"); // window.open is overridden so popups become a new pane rather than vanishing. diff --git a/lib/src/host/iframe-proxy-rewrite.ts b/lib/src/host/iframe-proxy-rewrite.ts index 8fd26b2b4..18af02016 100644 --- a/lib/src/host/iframe-proxy-rewrite.ts +++ b/lib/src/host/iframe-proxy-rewrite.ts @@ -89,13 +89,15 @@ export function frameAncestorsCsp(embedderOrigins: string[]): string { // user-supplied, so it is not an eval vector. Injected inline into served HTML; // this is why the default drops upstream CSP whole rather than per-directive (an // inline script needs `script-src` gone as much as the frame needs -// `frame-ancestors` gone). It posts four message kinds to the Wall and nothing +// `frame-ancestors` gone). It posts five message kinds to the Wall and nothing // else (every other keystroke flows to the tool). A nested document relays the // three pane-level kinds through its same-origin proxy parents until the outer // document reaches the app; its document-level location stays inside the outer // frame: // - `leader`: the reserved dual-tap ⌘/⇧ chord (matching handle-dual-tap.ts), -// so the global chord keeps working with the frame focused. +// so the global chord keeps working with the frame focused. Any other key +// cancels a pending tap, as in use-wall-keyboard.ts: a left-Shift capital, +// a word, then a right-Shift capital is typing. // - `pointerdown`: a click landed in the frame. A cross-origin click reaches // only the frame, so the Wall can't see it; this lets it select the pane / // enter passthrough. It's genuine user input, so it can't loop with the @@ -106,6 +108,8 @@ export function frameAncestorsCsp(embedderOrigins: string[]): string { // - `open-window`: an anchor with any target but `_self` (`_blank`, `_top`, // `_parent`, a named frame) or a `window.open` the single-frame renderer // can't honor; the parent offers it as a new pane instead. +// - `theme-request`: the document loaded and wants the app's theme; it stays +// in the outer document. export function iframeShim(embedderOrigin: string): string { return `(function(){ var P=window.parent; @@ -171,6 +175,7 @@ export function iframeShim(embedderOrigin: string): string { addEventListener('keydown',function(e){ if(e.key==='Meta'){if(tap(cmd,e))post('leader');} else if(e.key==='Shift'){if(tap(shift,e))post('leader');} + else cmd.side=shift.side=null; },true); addEventListener('pointerdown',function(){post('pointerdown');},true); addEventListener('click',function(e){