Merge pull request #345 from saphid/fix/framey-panel-status-session

Scope Framey panel status and input to active session
This commit is contained in:
Alex Southwell authored and GitHub committed 2026-10-26 23:42:21 +00:00
commit 5741ae6243
5 files changed
+39 -7

No files matched your search

+3
View File
@@ -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
+18
View File
@@ -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();
+9 -1
View File
@@ -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();
+4 -3
View File
@@ -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();
+5 -3
View File
@@ -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;