From 1824b54140e3c2b4ef7f3d7b746a36577ab56dc5 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 22:38:05 +0000 Subject: [PATCH] Scope Framey panel status and input to active session --- docs/framey-loader.md | 3 +++ tests/page/framey_event_parent.mjs | 18 ++++++++++++++++++ tests/page/framey_sample.mjs | 10 +++++++++- ui/framey-host.js | 7 ++++--- ui/headset.html | 8 +++++--- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/framey-loader.md b/docs/framey-loader.md index 5b42e84..e2ffe0f 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -224,6 +224,9 @@ chunks from two installed versions. The headset also includes the panel's store SHA-256 receipt with each backend call, Steam read, and event poll. It rejects a request if the installed receipt changed before the next status refresh; reopen the panel to use the new version. +Panel-ready, error, and trusted-pointer reports also carry the current panel +session. A delayed report from a closed panel cannot change the new panel's +status or count as input to that panel. ## For plugin authors diff --git a/tests/page/framey_event_parent.mjs b/tests/page/framey_event_parent.mjs index 8d68865..2962bf5 100644 --- a/tests/page/framey_event_parent.mjs +++ b/tests/page/framey_event_parent.mjs @@ -107,6 +107,24 @@ await send({type:'framey:scroll:result',session:scrollSession,direction:'down',m assert.equal(el('frameyPanelStatus').textContent,'Scrolled plugin down'); await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:false}); assert.equal(el('frameyPanelStatus').textContent,'No more plugin content in that direction'); +// WindowProxy also survives panel navigation for lifecycle and input messages. +// An old iframe must not mark a replacement ready, hide its error, or record +// controller evidence for the new panel. +const beforeStatusRequests=requests.length; +for(const type of ['framey:ready','framey:input','framey:error']) { + await send({type,session:'old',message:'old panel failed'}); + await send({type,message:'missing session'}); +} +assert.equal(requests.length,beforeStatusRequests); +assert.equal(el('frameyPanelStatus').textContent,'No more plugin content in that direction'); +await send({type:'framey:ready',session:scrollSession}); +assert.equal(el('frameyPanelStatus').textContent,'Plugin panel ready'); +assert.equal(requests.at(-1).path,'/api/framey/check'); +assert.equal(requests.at(-1).body.kind,'panel'); +await send({type:'framey:input',session:scrollSession}); +assert.equal(requests.at(-1).body.kind,'input'); +await send({type:'framey:error',session:scrollSession,message:'current panel failed'}); +assert.equal(el('frameyPanelStatus').textContent,'Plugin panel unavailable: current panel failed'); el('frameyClose').onclick(); const replyCount=replies.length; el('frameyScrollUp').onclick(); diff --git a/tests/page/framey_sample.mjs b/tests/page/framey_sample.mjs index 69fa5fd..71c8ba6 100644 --- a/tests/page/framey_sample.mjs +++ b/tests/page/framey_sample.mjs @@ -20,13 +20,15 @@ const elements=new Map(), roots=new Map(); const document={getElementById:id=>{ if(!elements.has(id))elements.set(id,new Element());return elements.get(id); },createElement:()=>new Element()}; -const window={React,ReactDOM:{createRoot:node=>{ +const window={React,location:{hash:'#sample-session'},ReactDOM:{createRoot:node=>{ const root={render(value){this.value=value;},unmount(){this.unmounted=true;}}; roots.set(node,root);return root; }},addEventListener(){}}; const listeners=[]; const calls=[]; +const messages=[]; const parent={postMessage(message){ + messages.push(message); if(message.type!=='framey:call')return; calls.push(message); listeners.find(x=>x.type==='message').handler({source:parent, @@ -36,6 +38,12 @@ const context=vm.createContext({window,document, navigator:{language:'en'},parent,addEventListener:(type,handler)=>listeners.push({type,handler}), setTimeout:()=>1,clearTimeout(){}}); vm.runInContext(host,context); +const pointerup=listeners.find(listener=>listener.type==='pointerup').handler; +pointerup({isTrusted:false}); +assert.equal(messages.some(message=>message.type==='framey:input'),false); +pointerup({isTrusted:true}); +assert.equal(messages.at(-1).type,'framey:input'); +assert.equal(messages.at(-1).session,'sample-session'); Object.assign(context,{SP_REACT:window.SP_REACT,SP_JSX:window.SP_JSX,DFL:window.DFL}); vm.runInContext(bundle.replace(/export \{ index as default \};/, 'window.sample = index;'),context); const plugin=window.sample(); diff --git a/ui/framey-host.js b/ui/framey-host.js index 26f89c6..037bb7e 100644 --- a/ui/framey-host.js +++ b/ui/framey-host.js @@ -190,7 +190,7 @@ parent.postMessage({type:'framey:events:close', session:cacheSession}, '*'); }); addEventListener('pointerup', event => { - if (event.isTrusted) parent.postMessage({type:'framey:input'}, '*'); + if (event.isTrusted) parent.postMessage({type:'framey:input', session:cacheSession}, '*'); }, {passive:true}); addEventListener('pointermove', event => { if (event.isTrusted) lastScrollTarget = scrollTarget(event.target); @@ -829,10 +829,11 @@ if (!plugin?.content && !plugin?.settings) throw Error('The Decky plugin has no panel to display'); window.ReactDOM.createRoot(root).render(R.createElement(PanelBoundary, null, R.createElement(PluginSurface, {plugin}))); - parent.postMessage({type:'framey:ready'}, '*'); + parent.postMessage({type:'framey:ready', session:cacheSession}, '*'); } catch (failure) { error.textContent = `This Decky panel is not compatible with Framey: ${failure.message}`; - parent.postMessage({type:'framey:error', message:failure.message}, '*'); + parent.postMessage({type:'framey:error', session:cacheSession, + message:failure.message}, '*'); } } start(); diff --git a/ui/headset.html b/ui/headset.html index 856a9e8..23b2ccb 100644 --- a/ui/headset.html +++ b/ui/headset.html @@ -292,10 +292,12 @@ window.addEventListener('message',async event=>{ if(req.session===frameySession)frameyCacheSave(activeFrameyId,req.entries); return; } - if(req.type==='framey:ready'){byId('frameyPanelStatus').textContent='Plugin panel ready'; + if(req.type==='framey:ready'&&req.session===frameySession){byId('frameyPanelStatus').textContent='Plugin panel ready'; api('/api/framey/check',{kind:'panel',pluginId:activeFrameyId}).catch(()=>{});return;} - if(req.type==='framey:input'){api('/api/framey/check',{kind:'input'}).catch(()=>{});return;} - if(req.type==='framey:error'){byId('frameyPanelStatus').textContent=`Plugin panel unavailable: ${String(req.message).slice(0,200)}`;return;} + if(req.type==='framey:input'&&req.session===frameySession){ + api('/api/framey/check',{kind:'input'}).catch(()=>{});return;} + if(req.type==='framey:error'&&req.session===frameySession){ + byId('frameyPanelStatus').textContent=`Plugin panel unavailable: ${String(req.message).slice(0,200)}`;return;} if(req.type==='framey:running:close'){ if(req.session===frameySession&&frameyRunningActive){ frameyRunningActive=false;