Compare commits

...
Author SHA1 Message Date
SAPHID 2fed8a8133 Merge remote-tracking branch 'origin/main' into feat/framey-vr-store
# Conflicts:
#	docs/framey-loader.md
#	tests/test_framey_panel.py
#	ui/headset.html
2026-10-25 07:18:23 +00:00
SAPHID a22295ff76 Browse and search the full Decky store in VR 2026-10-25 07:07:47 +00:00
SAPHID 402428e86b Merge remote-tracking branch 'origin/main' into review/framey-vr
# Conflicts:
#	docs/current-support-matrix.md
2026-10-25 07:05:45 +00:00
SAPHID 79c4f2c208 Lock Frame before worn checks and clean private captures 2026-10-25 07:04:15 +00:00
SAPHID d392687237 Record Framey VR verification limits 2026-10-25 06:52:29 +00:00
SAPHID 389c8617e8 Host Decky plugin panels in Frame Control VR 2026-10-25 06:50:20 +00:00
6 changed files with 88 additions and 9 deletions

No files matched your search

+7
View File
@@ -54,6 +54,13 @@ has no supported injection target in the Frame Control panel.
The [browser and fake Frame evidence](evidence/framey-browser-2026-10-25.md)
records exactly what was exercised.
The in-headset **Open official store** view loads the full official catalogue.
Search matches names, authors, and descriptions. Filters show non-root plugins
or installed plugins, and Previous/Next buttons browse 20 results per page.
The root label is a compatibility warning; other APIs are checked only when a
plugin is opened. The store still needs the managed Decky user service running
before its Install or Update buttons are enabled.
## For plugin authors
Publish a normal [Decky store](https://plugins.deckbrew.xyz/) API v2 release
+45
View File
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
const html = readFileSync(new URL('../../ui/headset.html', import.meta.url), 'utf8');
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1].split('refresh();refreshGames();')[0];
class Element {
constructor(id='') {this.id=id; this.value=''; this.textContent=''; this.children=[]; this.disabled=false; this.hidden=false;}
append(...nodes) {this.children.push(...nodes);}
replaceChildren(...nodes) {this.children=nodes; this.textContent='';}
scrollIntoView() {}
}
const elements = new Map();
elements.set('deckyStoreFilter',Object.assign(new Element('deckyStoreFilter'),{value:'all'}));
const document = {getElementById(id) {if(!elements.has(id)) elements.set(id,new Element(id)); return elements.get(id);},
createElement() {return new Element();}, hidden:false};
const ctx = vm.createContext({document, location:{hash:'',pathname:'/'},
sessionStorage:{getItem:()=>'',setItem(){}}, history:{replaceState(){}},
window:{addEventListener(){}}, setInterval(){}, setTimeout(){},
fetch(){throw Error('No network expected');}, console});
vm.runInContext(script,ctx);
const el=id=>document.getElementById(id);
const fixtures=Array.from({length:43},(_,i)=>({id:i+1,name:`Plugin ${i+1}`,author:i===42?'Special Author':'Maker',
description:'A panel',version:'1.0',root:i===0}));
ctx.fixtures=fixtures;
vm.runInContext('deckyStore=fixtures; deckyState={running:true,plugins:[{storeId:2,version:"1.0"}]}; showDeckyStore()',ctx);
assert.equal(el('deckyStoreSummary').textContent,'43 plugins found');
assert.equal(el('deckyStoreList').children.length,20);
assert.equal(el('deckyStorePage').textContent,'Page 1 of 3');
el('deckyStoreNext').onclick();
assert.equal(el('deckyStoreList').children.length,20);
el('deckyStoreNext').onclick();
assert.equal(el('deckyStoreList').children.length,3);
assert.equal(el('deckyStoreNext').disabled,true);
el('deckySearch').value='special author'; el('deckySearch').oninput();
assert.equal(el('deckyStoreSummary').textContent,'1 plugin found');
assert.match(el('deckyStoreList').children[0].children[0].textContent,/Plugin 43/);
assert.equal(el('deckyStorePage').textContent,'Page 1 of 1');
el('deckySearch').value=''; el('deckyStoreFilter').value='installed'; el('deckyStoreFilter').onchange();
assert.equal(el('deckyStoreSummary').textContent,'1 plugin found');
assert.match(el('deckyStoreList').children[0].children[0].textContent,/Plugin 2/);
el('deckyStoreFilter').value='compatible'; el('deckyStoreFilter').onchange();
assert.equal(el('deckyStoreSummary').textContent,'42 plugins found');
el('deckySearch').value='no matching plugin'; el('deckySearch').oninput();
assert.equal(el('deckyStoreList').textContent,'No plugins match this search.');
+2 -2
View File
@@ -30,14 +30,14 @@ def archive(flags=(), extra=None):
class ArchiveTests(unittest.TestCase):
def test_catalog_selects_valid_visible_latest_versions(self):
records = [{'id': 42, 'name': 'Example', 'visible': True, 'versions':
records = [{'id': 42, 'name': 'Example', 'author': 'Panel Author', 'visible': True, 'versions':
[{'name': '1.0', 'hash': 'a' * 64}], 'tags': []},
{'id': 43, 'name': 'Hidden', 'visible': False, 'versions':
[{'name': '1.0', 'hash': 'b' * 64}]},
{'id': 44, 'name': 'Unsafe', 'visible': True, 'versions':
[{'name': '1.0; touch /tmp/bad', 'hash': 'c' * 64}]}]
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
self.assertEqual([p['id'] for p in frame_decky_store.catalog()], [42])
self.assertEqual([(p['id'], p['author']) for p in frame_decky_store.catalog()], [(42, 'Panel Author')])
def test_accepts_hashed_non_root_archive(self):
data, digest = archive()
+6
View File
@@ -5,6 +5,7 @@ import http.client
import io
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import tarfile
@@ -19,6 +20,11 @@ import server
class Panel(unittest.TestCase):
def test_vr_store_browsing_and_search(self):
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_store.mjs')],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
+2
View File
@@ -54,8 +54,10 @@ def catalog():
if not latest:
continue
tags = item.get('tags') if isinstance(item.get('tags'), list) else []
author = item.get('author') if isinstance(item.get('author'), str) else ''
result.append({'id': item['id'], 'name': item['name'][:100],
'description': str(item.get('description') or '')[:300],
'author': author[:100],
'version': latest['name'][:40], 'sha256': latest['hash'],
'root': 'root' in tags})
return result
+26 -7
View File
@@ -46,7 +46,10 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
<h2>Decky plugins</h2><p>Manage official store plugins here. Decky's Quick Access Menu is absent from this VR shell. Framey can show compatible plugin panels and settings below; Steam library and store page patches are unavailable.</p>
<div class="row"><button id="deckyRefresh" class="secondary">Refresh plugins</button><button id="deckyStoreLoad" class="secondary">Open official store</button></div>
<p id="deckyState" role="status"></p><div id="deckyInstalled" class="stack"></div>
<div id="deckyStorePanel" hidden><label>Find a plugin <input id="deckySearch" type="search" placeholder="Search Decky store"></label><div id="deckyStoreList" class="stack"></div></div>
<div id="deckyStorePanel" hidden><div class="row" style="margin:16px 0"><label>Find a plugin <input id="deckySearch" type="search" placeholder="Name, author or description"></label>
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">Without root requirement</option><option value="installed">Installed plugins</option></select></label></div>
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
@@ -97,7 +100,7 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
catch(error){byId('actionStatus').textContent=error.message;quit.disabled=false;}};
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
}catch(error){list.textContent=error.message;}}
let deckyState=null, deckyStore=[],activeFrameyId=null;
let deckyState=null, deckyStore=[], deckyStoreIndex=0, activeFrameyId=null;
async function refreshDecky(){try{deckyState=await api('/api/decky');
byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.':'Decky is not running. Install it from Frame Control on your computer.';
const list=byId('deckyInstalled');list.replaceChildren();
@@ -109,13 +112,25 @@ async function refreshDecky(){try{deckyState=await api('/api/decky');
button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);}
list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.';
showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}}
async function loadDeckyStore(){try{const data=await api('/api/decky/store');deckyStore=data.plugins;
byId('deckyStorePanel').hidden=false;showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}}
async function loadDeckyStore(){byId('deckyStorePanel').hidden=false;
byId('deckyStoreSummary').textContent='Loading official Decky store…';
try{const data=await api('/api/decky/store');deckyStore=data.plugins;deckyStoreIndex=0;showDeckyStore();}
catch(error){byId('deckyStoreSummary').textContent=`Store unavailable: ${error.message}`;}}
function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren();
const q=byId('deckySearch').value.trim().toLowerCase();
for(const plugin of deckyStore.filter(p=>!q||`${p.name} ${p.description}`.toLowerCase().includes(q)).slice(0,30)){
const filter=byId('deckyStoreFilter').value;
const found=deckyStore.filter(p=>(!q||`${p.name} ${p.author||''} ${p.description}`.toLowerCase().includes(q))&&
(filter==='all'||(filter==='compatible'&&!p.root)||(filter==='installed'&&(deckyState?.plugins||[]).some(x=>x.storeId===p.id))));
const size=20, pages=Math.max(1,Math.ceil(found.length/size));
deckyStoreIndex=Math.min(deckyStoreIndex,pages-1);
byId('deckyStoreSummary').textContent=`${found.length} plugin${found.length===1?'':'s'} found`;
byId('deckyStorePage').textContent=`Page ${deckyStoreIndex+1} of ${pages}`;
byId('deckyStorePrevious').disabled=deckyStoreIndex===0;
byId('deckyStoreNext').disabled=deckyStoreIndex>=pages-1;
if(!found.length)list.textContent='No plugins match this search.';
for(const plugin of found.slice(deckyStoreIndex*size,(deckyStoreIndex+1)*size)){
const row=document.createElement('div');row.className='block game';
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version} · ${plugin.description}`;
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version}${plugin.author?' · '+plugin.author:''} · ${plugin.description}`;
const button=document.createElement('button');button.textContent='Install';
const installed=(deckyState?.plugins||[]).find(p=>p.storeId===plugin.id);
if(installed)button.textContent=installed.version===plugin.version?'Reinstall':'Update';
@@ -183,7 +198,11 @@ byId('statusScreenClose').onclick=()=>{byId('statusScreen').hidden=true;byId('st
byId('statusScreenRefresh').onclick=refresh;
window.addEventListener('keydown',event=>{if(event.key==='Escape'&&!byId('statusScreen').hidden)byId('statusScreenClose').click();});
byId('gamesRefresh').onclick=refreshGames;byId('record').onclick=toggleRecord;byId('extensionsRefresh').onclick=refreshExtensions;
byId('deckyRefresh').onclick=refreshDecky;byId('deckyStoreLoad').onclick=loadDeckyStore;byId('deckySearch').oninput=showDeckyStore;
byId('deckyRefresh').onclick=refreshDecky;byId('deckyStoreLoad').onclick=loadDeckyStore;
byId('deckySearch').oninput=()=>{deckyStoreIndex=0;showDeckyStore();};
byId('deckyStoreFilter').onchange=()=>{deckyStoreIndex=0;showDeckyStore();};
byId('deckyStorePrevious').onclick=()=>{deckyStoreIndex--;showDeckyStore();};
byId('deckyStoreNext').onclick=()=>{deckyStoreIndex++;showDeckyStore();};
byId('extensionClose').onclick=()=>{activeExtension=null;byId('extensionFrame').removeAttribute('src');byId('extensionPanel').hidden=true;};
window.addEventListener('message',event=>{if(!activeExtension||event.source!==byId('extensionFrame').contentWindow)return;
const request=event.data;if(!request||request.type!=='frame-control:request'||!Number.isSafeInteger(request.id))return;