Compare commits

...
Author SHA1 Message Date
Alex Southwell 5239dae1a8 Merge pull request #268 from saphid/release/v0.4.37
Prepare Frame Control v0.4.37
2026-10-26 00:25:20 +00:00
SAPHID 7ed08c5bbd Document Framey toast limits for 0.4.37 2026-10-26 00:14:17 +00:00
SAPHID 449146bbdb Merge remote-tracking branch 'origin/main' into release/v0.4.37 2026-10-26 00:14:04 +00:00
Alex Southwell e115006ac9 Merge pull request #267 from saphid/fix/framey-react-toasts
Framey: render React content in plugin toasts
2026-10-26 00:13:48 +00:00
SAPHID 30fbadbb23 Prepare Frame Control 0.4.37 2026-10-26 00:12:23 +00:00
SAPHID 587a7fc8c3 Render React content in Framey panel toasts 2026-10-26 00:06:00 +00:00
6 changed files with 67 additions and 15 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.36",
"version": "0.4.37",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.36",
"version": "0.4.37",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.36",
"version": "0.4.37",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
+4 -4
View File
@@ -1,6 +1,6 @@
# What we have tested
Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
Current for Frame Control **v0.4.37**, 26 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|---|---|---|---|---|
@@ -22,8 +22,8 @@ Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical
| Passthrough in recordings | Real room pixels in OBS and a production MP4 after safe SteamVR wake | No | **Yes** for the checked 0.5.6 mirror. Raw cameras are not opened. Other games/modes and Steam clips may differ | 25 Oct: [validation](evidence/obs-validation-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
| Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
| Experimental Frame Control SteamVR panel | Real Frame OS 0.5.6 installed its user service, registered an overlay, served authenticated live status and a read-only extension, saved a screenshot and short MP4, and removed owned resources | No | Headset was unworn in standby: overlay reported `not_visible`, stereo capture was dark; wearer-visible pixels and controller input are unverified. The new large read-only status screen has browser layout and packaging checks only | 25 Oct, v0.4.29 device check and v0.4.31 status screen: [PR #198](https://github.com/saphid/frame-control/pull/198), [PR #201](https://github.com/saphid/frame-control/pull/201), [PR #220](https://github.com/saphid/frame-control/pull/220), [panel guide](headset-app.md) |
| Experimental Decky Loader manager | Real Frame OS 0.5.6 ARM64 user-service install, status, update and remove completed; seven official-store plugin backends loaded, and ProtonDB Badges completed managed store install/update/remove | No | v0.4.33 shared transaction lock and stopped-loader plugin removal have Linux and CI tests, but no live recovery test on the Frame. v0.4.35 treats `_root` store tags as root-required and hides duplicate IDs; this has test coverage, not a live store check. v0.4.36 stops a store install or update when the catalog hash changed after review; this was tested with a fake Frame, not with a live store change on the headset. Plugin pages, settings, routes, toasts and controller input in VR remain unverified; some plugins need root, x86_64 binaries or Deck-specific hardware | 25 Oct, v0.4.31 hardware and v0.4.36 tests: [PR #208](https://github.com/saphid/frame-control/pull/208), [PR #230](https://github.com/saphid/frame-control/pull/230), [PR #234](https://github.com/saphid/frame-control/pull/234), [PR #246](https://github.com/saphid/frame-control/pull/246), [PR #253](https://github.com/saphid/frame-control/pull/253), [compatibility table](decky-frame.md) |
| Experimental Framey VR plugin panels and store | No live real-Frame panel test | No | ProtonDB Badges settings rendered and sent a setting call in local Chromium. Official Steam Status rendered in local Chromium; a session-only cache avoided its earlier storage errors. A later local browser check restored a bounded per-plugin cache after panel reopen; live Decky and offline behavior were not tested. Fake Frame tests cover setting read/write through update and reinstall and install/remove two pinned official API v2 archives plus the modern API v1 Storage Cleaner archive. Page tests cover the official JSX runtime, disabled controls, text fields, sliders, grouped dropdowns and an author sample bundle with a settings tab and read-only backend quick action. The sample is not published in the store. The 11-archive manifest audit found only ProtonDB Badges API v2 in that set. Storage Cleaner 1.4.0 showed an empty-data panel and four disabled cleanup buttons in local Chromium with a stub backend; no cleanup action was tested. HLTB for Deck 2.0.12 built a shallow settings view and Bluetooth 3.1.1 built its initial panel in local frontend tests without effects or backend calls. Their badges, search, pairing and Bluetooth changes remain unverified. A hash match, local panel and fake Frame install do not prove VR use. Wearer-visible pixels, controller input and live settings persistence remain unverified | 25 Oct, v0.4.32–0.4.36: [#223](https://github.com/saphid/frame-control/pull/223), [#224](https://github.com/saphid/frame-control/pull/224), [#225](https://github.com/saphid/frame-control/pull/225), [#226](https://github.com/saphid/frame-control/pull/226), [#227](https://github.com/saphid/frame-control/pull/227), [#229](https://github.com/saphid/frame-control/pull/229), [#231](https://github.com/saphid/frame-control/pull/231), [#232](https://github.com/saphid/frame-control/pull/232), [#233](https://github.com/saphid/frame-control/pull/233), [#236](https://github.com/saphid/frame-control/pull/236), [#237](https://github.com/saphid/frame-control/pull/237), [#238](https://github.com/saphid/frame-control/pull/238), [#239](https://github.com/saphid/frame-control/pull/239), [#240](https://github.com/saphid/frame-control/pull/240), [#241](https://github.com/saphid/frame-control/pull/241), [#243](https://github.com/saphid/frame-control/pull/243), [#244](https://github.com/saphid/frame-control/pull/244), [#245](https://github.com/saphid/frame-control/pull/245), [#247](https://github.com/saphid/frame-control/pull/247), [#248](https://github.com/saphid/frame-control/pull/248), [#251](https://github.com/saphid/frame-control/pull/251), [#252](https://github.com/saphid/frame-control/pull/252), [#253](https://github.com/saphid/frame-control/pull/253), [browser/fake Frame evidence](evidence/framey-browser-2026-10-25.md), [guide](framey-loader.md) |
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 25 Oct, v0.4.29–0.4.37: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [worn checklist](framey-vr-check.md) |
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service later returned read-only ProtonDB and Steam Status setting RPCs through Steam CEF; it was removed under the shared lock | No | The loader remains opt-in. The exact-version official catalog covers 116 plugins: 0 works, 43 partial, 62 broken and 11 not applicable. Fake Frame transport and local Chromium do not prove official plugin backends or VR controls. Physical controller use and wearer-visible official plugin panels remain unverified | 25–26 Oct, v0.4.31–0.4.37: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend evidence](evidence/framey-official-backend-2026-10-25.json), [compatibility table](framey-compatibility.md) |
| Experimental Framey VR plugin panels and store | An unattended Frame OS 0.5.6 run showed the Framey sample panel in stereo compositor and mirror captures. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility are unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. React toast content is tested in the host and fake Frame only; it is a panel notice, not a Steam system notification. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; 96 local Chromium initializers were ready and 20 failed. No plugin is marked fully working | 25–26 Oct, v0.4.37: [unattended evidence and cleanup](evidence/framey-vr-2026-10-25/README.md), [CSS/SteamGridDB PR #257](https://github.com/saphid/frame-control/pull/257), [legacy bridge PR #261](https://github.com/saphid/frame-control/pull/261), [UI shims PR #263](https://github.com/saphid/frame-control/pull/263), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [toast PR #267](https://github.com/saphid/frame-control/pull/267), [worn checklist](framey-vr-check.md) |
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
+1 -1
View File
@@ -68,7 +68,7 @@ none of these frontend controls has passed a worn-headset check yet.
| `DialogCheckbox`, `DialogButtonPrimary` | Native checkbox and button controls in the panel | Host unit test; Storage Cleaner empty-data browser check |
| Optional `settings`, `frameyQuickActions` | Separate settings tab; up to eight actions with result text | Host unit test; these fields are Framey additions |
| `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e for setting read/write across CEF calls, update, and reinstall; host unit test. Real Decky persistence remains unverified. |
| `toaster.toast` | Visible toast inside the plugin panel; `dismiss` and optional `onClick` | Host unit test; no Steam system notification |
| `toaster.toast` | Visible toast inside the plugin panel; plain text or React nodes for title, body, icon, logo and subtext; `dismiss` and optional `onClick` | Host unit test; no Steam system notification or worn VR check |
| `localStorage` | Synchronous, per-plugin cache backed by the Frame Control panel origin. Up to 64 keys, 64 KiB per value and 128 KiB total; `clear()` removes only this plugin's cache. Falls back to memory if panel storage is denied. | Parent/iframe round-trip and reopen in the local host test; real headset persistence remains unverified. |
| IndexedDB / localForage | IndexedDB is deliberately absent inside the opaque iframe so feature detection chooses the bounded localStorage driver. Plugins requiring IndexedDB directly remain unsupported. | HLTB official frontend in local Chromium: its previous `IDBFactory.open` SecurityError disappeared. No live HLTB data request was run. |
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values | Host unit test; VR navigation unverified |
+30 -1
View File
@@ -12,7 +12,11 @@ class Element {constructor(){this.children=[];this.textContent='';this.attribute
append(...children){this.children.push(...children);}setAttribute(key,value){this.attributes[key]=value;}
remove(){this.removed=true;}}
const elements=new Map();
const window={React,ReactDOM:{createRoot:()=>({render(){}})},addEventListener(){}};
const roots=new Map();
const window={React,ReactDOM:{createRoot:node=>{
const root={render(value){this.value=value;},unmount(){this.unmounted=true;}};
roots.set(node,root);return root;
}},addEventListener(){}};
const listeners=[];const messages=[];const parent={postMessage:message=>messages.push(message)};
const context=vm.createContext({window,document:{getElementById:id=>{
if(!elements.has(id))elements.set(id,new Element());return elements.get(id);},createElement:()=>new Element()},
@@ -101,6 +105,31 @@ assert.equal(toastNode.attributes.role,'status');
assert.equal(toastNode.children[0].textContent,'Saved');
assert.equal(toastNode.children[1].textContent,'Mode updated');
toast.dismiss();assert.equal(toastNode.removed,true);
// Decky's toast fields are ReactNode values. Keep JSX in the panel instead of
// showing "[object Object]", and release their React roots on dismissal.
const heading=React.createElement('em',null,'Saved');
const detail=React.createElement('span',null,'Mode updated');
const icon=React.createElement('svg',null);
const rich=api.toaster.toast({title:heading,body:[detail,' again'],icon,
subtext:'Open settings',showToast:true});
const richNode=elements.get('framey-toasts').children[1];
assert.equal(roots.get(richNode.children[0]).value,icon);
assert.equal(roots.get(richNode.children[1]).value,heading);
assert.deepEqual(Array.from(roots.get(richNode.children[2]).value),[detail,' again']);
assert.equal(richNode.children[3].textContent,'Open settings');
rich.dismiss();rich.dismiss();
assert.equal(richNode.removed,true);
for(const node of richNode.children.slice(0,3))assert.equal(roots.get(node).unmounted,true);
let opened=0;
const actionable=api.toaster.toast({title:'Open',onClick:()=>opened++});
const actionNode=elements.get('framey-toasts').children[2];
actionNode.children.at(-1).onclick();
assert.equal(opened,1);
assert.equal(actionNode.removed,true);
actionable.dismiss();
const hidden=api.toaster.toast({title:heading,showToast:false});
assert.equal(elements.get('framey-toasts').children.length,3);
hidden.dismiss();
function LocalRoute(){return React.createElement('p',null,'Route content');}
const undo=api.routerHook.addRoute('/plugin/details',LocalRoute);
+29 -6
View File
@@ -279,16 +279,39 @@
};
DFL.Router = {...DFL.Navigation, MainRunningApp:undefined};
DFL.DialogButton.render = () => ({type: DFL.DialogButton});
const toaster = {toast: ({title, body, duration, showToast=true, onClick}={}) => {
const data = {title, body};
const toaster = {toast: ({title, body, subtext, icon, logo, duration, showToast=true, onClick}={}) => {
const data = {title, body, subtext, icon, logo};
if (!showToast) return {data, dismiss(){}};
const item = document.createElement('section');
item.setAttribute('role','status');
const heading = document.createElement('strong'); heading.textContent = String(title || 'Plugin notice');
const detail = document.createElement('p'); detail.textContent = String(body || '');
const roots = [];
const addContent = (tag, value) => {
if (value === null || value === undefined || value === false) return;
const node = document.createElement(tag);
if (R.isValidElement(value) || Array.isArray(value)) {
const root = window.ReactDOM.createRoot(node);
roots.push(root);
root.render(value);
} else if (typeof value === 'string' || typeof value === 'number') {
node.textContent = String(value);
} else return;
item.append(node);
};
addContent('span', logo);
addContent('span', icon);
addContent('strong', title || 'Plugin notice');
addContent('p', body);
addContent('small', subtext);
const close = document.createElement('button'); close.type='button'; close.textContent='Dismiss';
const dismiss = () => {item.remove(); clearTimeout(timer);};
close.onclick=dismiss; item.append(heading, detail, close);
let dismissed = false;
const dismiss = () => {
if (dismissed) return;
dismissed = true;
clearTimeout(timer);
for (const root of roots) root.unmount();
item.remove();
};
close.onclick=dismiss; item.append(close);
if (typeof onClick === 'function') {
const open = document.createElement('button'); open.type='button'; open.textContent='Open';
open.onclick=()=>{onClick(); dismiss();}; item.append(open);