Files
saphid--frame-control/docs/framey-loader.md
T
Alex SouthwellandSAPHID 289a971c90 Add Decky settings sample for Framey plugin authors (#233)
* Provide Decky automatic JSX runtime in Framey panel

* Add Decky settings sample for Framey authors

* Use official Decky settings path in sample backend

---------

Co-authored-by: SAPHID <saphid@users.noreply.github.com>
2026-10-25 09:39:01 +00:00

8.4 KiB

Framey loader on Steam Frame

Framey is an experimental bridge for Decky store plugins inside Frame Control's SteamVR dashboard panel. It is opt in. The panel and Decky backend are ordinary user services under ~/.local/share/frame-control; neither needs root or a read-write SteamOS root filesystem. Decky's standard Quick Access menu still does not appear in the separate SteamVR CEF target.

Use in the headset

Install Frame Control in VR and Decky Loader on Frame from Frame Control on your computer. Open the Frame Control tab in SteamVR, then choose Decky plugins → Open official store. Search the catalogue, confirm an install, and choose Open panel from the installed plugin list. The panel contains the plugin's own controls and settings. Update and Remove affect only plugin folders that Frame Control's store manager installed. Removing a plugin keeps its Decky settings. Removing the Frame Control headset panel or Decky backend keeps plugin folders until each is removed separately.

Frame Control's existing official-store manager downloads a published ZIP from Decky's content-addressed CDN, verifies its store SHA-256 hash and manifest, rejects unsafe ZIP members and root plugins, and swaps one managed plugin folder with rollback on failure. It rejects non-ARM64 ELF and Windows binaries. The Decky user service restarts after a file change. Framey's panel host accepts only API v2 bundles with dist/index.js; other installed plugins remain manageable but have no Framey panel. The store manager does not currently screen a plugin's runtime network downloads.

What the compatibility host supplies

The plugin bundle runs in an opaque-origin iframe in the Frame Control SteamVR panel. It receives React 18.3.1, a small DFL panel-control shim, and the Decky API v2 call/callable interface. Backend methods are sent by the authenticated parent to Steam's existing SharedJSContext CDP client, then through Decky's existing websocket connection. Framey never opens a second Decky websocket, because Decky allows only one frontend connection. The iframe does not receive Frame Control's UI key. React and ReactDOM are bundled for offline use under their MIT licenses in ui/vendor.

The host currently supplies sections, rows, buttons, toggles, dropdowns, scroll containers, a modal, locale information, local panel routes and toasts, and backend calls for the plugin that owns the panel. Steam library/store page patches, global Steam UI components, file pickers, and privileged root methods have no VR target. A plugin that depends on them can render only part of its UI or show an incompatibility message. A Decky plugin's Python backend still runs as the headset user when Decky loads it, so review third-party code before opting in. The panel iframe can make HTTPS requests subject to ordinary browser CORS.

ProtonDB Badges' current API v2 bundle rendered its settings in a local Chromium browser with a stubbed backend reply on 25 October 2026. Its controls accepted pointer input. This does not verify SteamVR visibility, controller input, live Decky settings persistence, or badges on Steam game pages. The last item has no supported injection target in the Frame Control panel. The browser and fake Frame evidence records exactly what was exercised.

The fake Frame integration test now writes a Decky setting through Framey's scoped backend endpoint, reads it in a new CEF request, updates the plugin, then checks it again. It also confirms removal blocks method calls and a reinstall can read the retained setting. This tests the bridge and fixture; real Decky settings persistence on a worn Frame is still unverified.

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. If a plugin prevents Decky from starting, Remove remains available for Frame Control-owned plugins while the managed service is stopped; it does not restart the service.

Install, Update, and Remove show the current job stage in the headset panel. The store verifies each downloaded archive and the headset applies it with a rollback if Decky cannot start. The panel remembers the current job ID for this browser session, so Check result can resume polling after a navigation or temporary connection error. Refresh the installed list before starting another operation after an error. If Frame Control restarts, it cannot recover the old job result. The panel clears that job ID and asks you to refresh the installed list before making another change.

For plugin authors

Start with the Framey settings sample for a buildable Decky API v2 panel and Python settings backend.

Publish a normal Decky store API v2 release with plugin.json, a single dist/index.js ESM frontend entry point, and a Python backend if needed. Give the frontend a content React element returned by definePlugin, and build settings as panel controls from @decky/ui. Backend calls through @decky/api reach only that plugin's own backend. For best VR compatibility, keep the panel self-contained and use PanelSection, PanelSectionRow, ButtonItem, ToggleField, and DropdownItem. Handle a missing Steam UI route, store overlay, or SteamClient object gracefully. Avoid root, remote binary downloads, Steam library patches, and assumptions about Steam Deck hardware. Declare settings defaults so a fresh user service can render without pre-existing files. Test your ZIP with the store validator and the fake Frame flow before asking users to install it.

An API v2 plugin's normal content is its default VR panel. Framey also shows an optional settings React element under a Settings tab and an optional titleView above it. For one-tap headset controls, return frameyQuickActions: [{label: 'Refresh data', onClick: refreshData}] from the plugin initializer. Framey displays up to eight actions and reports a pending, success, or failure state. These descriptor fields are Framey additions; Decky ignores them. Use a plugin backend method through @decky/api for work needing persistent state, and keep a quick action safe to repeat. See the API v2 compatibility table for tested behavior and remaining limits.

The Frame Control extension API is an alternative for a Frame-specific panel. It needs only a local manifest, HTML, and the read-only status bridge; it does not need Decky.

Removal

Use Remove on each installed Framey plugin, then Remove managed Decky and Remove under Frame Control in VR. These operations remove only Frame Control-owned folders and services. Settings, the Chromium profile, and other users' plugins are preserved. To remove one managed plugin manually, first inspect its .frame-control-store.json receipt in ~/.local/share/frame-control/decky/plugins/<folder>, then delete only that folder and run systemctl --user try-restart frame-control-decky.service. The Decky and headset service manual removal commands are in Decky on Frame and Frame Control in the headset.

Fast worn-headset check

With an already installed Framey plugin and someone wearing the headset, run python3 scripts/framey-worn-check.py from a paired computer. The script atomically acquires /tmp/frame-test.lock before reading battery or SteamVR activity. If another job owns the lock, it makes no sensor reads. It releases its own lock without starting the interaction when the headset is in standby. Within 35 seconds, use a controller to open a managed plugin panel and touch one setting. The script records a fresh trusted pointer event, the panel-ready event, SteamVR overlay visibility, and a stereo screenshot with varied pixels. It deletes its own short-lived challenge and releases only its own lock. It records the screenshot hash and color count, then removes the private temporary image from both devices. Pixel variety alone cannot prove the UI text was legible; a person must still confirm the view in the headset. The script does not install or remove a plugin, alter Steam settings, or wake the headset.