diff --git a/examples/framey-settings/README.md b/examples/framey-settings/README.md
index 6bcf845..55c9228 100644
--- a/examples/framey-settings/README.md
+++ b/examples/framey-settings/README.md
@@ -5,7 +5,12 @@ This is a small Decky API v2 plugin for authors. Its panel uses the official
backend method, stores one boolean under `decky.decky_SETTINGS_DIR`,
and reads it back when the panel opens. The backend rejects other keys and
values. The frontend also shows a local toast after a successful save. A
-missing, damaged, or old non-boolean value reads as the caller's default;
+Framey settings tab exposes the same toggle, and a read-only quick action
+fetches the saved value without changing it. These two fields are Framey
+additions; Decky's own menu still uses the main panel content. The quick
+action calls the backend, so it reads the persisted setting rather than a
+browser cache. A missing, damaged, or old non-boolean value reads as the
+caller's default;
the next save atomically replaces the sample's settings file.
## Build
@@ -29,7 +34,8 @@ plugin directory does not enable its VR panel.
Run `python3 -m unittest discover -s tests -p test_framey_sample.py` from the
repository root. It checks backend persistence and evaluates the built bundle
-against Framey's actual host script. The fake Frame store flow is exercised by
+against Framey's actual host script, including the quick action's scoped
+backend call. The fake Frame store flow is exercised by
the repository's e2e suite. No worn-headset visibility or controller-input test
has passed for this sample yet.
diff --git a/examples/framey-settings/dist/index.js b/examples/framey-settings/dist/index.js
index 565ec66..714534c 100644
--- a/examples/framey-settings/dist/index.js
+++ b/examples/framey-settings/dist/index.js
@@ -61,10 +61,21 @@ function Content() {
}
return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] });
}
-var index = definePlugin(() => ({
+var index = definePlugin(() => Object.assign({
name: 'Framey Settings Sample',
icon: SP_JSX.jsx("span", { "aria-hidden": "true", children: "\u2699" }),
content: SP_JSX.jsx(Content, {}),
+}, {
+ // Framey additions are ignored by Decky's own menu. Keep the main content
+ // usable there too, while showing authors the extra VR settings view.
+ settings: SP_JSX.jsx(Content, {}),
+ frameyQuickActions: [{
+ label: 'Read saved setting',
+ onClick: async () => {
+ const enabled = await getSetting('enabled', false);
+ return `Saved setting is ${enabled ? 'on' : 'off'}`;
+ },
+ }],
}));
export { index as default };
diff --git a/examples/framey-settings/dist/index.js.map b/examples/framey-settings/dist/index.js.map
index 469e504..cf55c3c 100644
--- a/examples/framey-settings/dist/index.js.map
+++ b/examples/framey-settings/dist/index.js.map
@@ -1 +1 @@
-{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, toaster} from '@decky/api';\nimport {ButtonItem, PanelSection, PanelSectionRow, ToggleField} from '@decky/ui';\nimport {useEffect, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n\n async function refresh() {\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample', body: 'Setting saved'});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n return \n \n {void change(value);}} />\n \n {void refresh();}}>Reload saved setting\n {message}
\n ;\n}\n\nexport default definePlugin(() => ({\n name: 'Framey Settings Sample',\n icon: ⚙,\n content: ,\n}));\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
\ No newline at end of file
+{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, toaster} from '@decky/api';\nimport {ButtonItem, PanelSection, PanelSectionRow, ToggleField} from '@decky/ui';\nimport {useEffect, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n\n async function refresh() {\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample', body: 'Setting saved'});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n setBusy(false);\n }\n }\n\n return \n \n {void change(value);}} />\n \n {void refresh();}}>Reload saved setting\n {message}
\n ;\n}\n\nexport default definePlugin(() => Object.assign({\n name: 'Framey Settings Sample',\n icon: ⚙,\n content: ,\n}, {\n // Framey additions are ignored by Decky's own menu. Keep the main content\n // usable there too, while showing authors the extra VR settings view.\n settings: ,\n frameyQuickActions: [{\n label: 'Read saved setting',\n onClick: async () => {\n const enabled = await getSetting('enabled', false);\n return `Saved setting is ${enabled ? 'on' : 'off'}`;\n },\n }],\n}));\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
\ No newline at end of file
diff --git a/examples/framey-settings/src/index.tsx b/examples/framey-settings/src/index.tsx
index b93a1f0..d7e5c5d 100644
--- a/examples/framey-settings/src/index.tsx
+++ b/examples/framey-settings/src/index.tsx
@@ -49,8 +49,19 @@ function Content() {
;
}
-export default definePlugin(() => ({
+export default definePlugin(() => Object.assign({
name: 'Framey Settings Sample',
icon: ⚙,
content: ,
+}, {
+ // Framey additions are ignored by Decky's own menu. Keep the main content
+ // usable there too, while showing authors the extra VR settings view.
+ settings: ,
+ frameyQuickActions: [{
+ label: 'Read saved setting',
+ onClick: async () => {
+ const enabled = await getSetting('enabled', false);
+ return `Saved setting is ${enabled ? 'on' : 'off'}`;
+ },
+ }],
}));
diff --git a/tests/page/framey_sample.mjs b/tests/page/framey_sample.mjs
index e54e2c3..d6104ba 100644
--- a/tests/page/framey_sample.mjs
+++ b/tests/page/framey_sample.mjs
@@ -10,8 +10,16 @@ const React={Fragment:Symbol('fragment'),Component:class {},
createElement:(type,props,...children)=>({type,props:props||{},children}),
useState:value=>[value,()=>{}],useEffect(){},isValidElement:value=>!!value?.type};
const window={React,ReactDOM:{createRoot:()=>({render(){}})},addEventListener(){}};
+const listeners=[];
+const calls=[];
+const parent={postMessage(message){
+ if(message.type!=='framey:call')return;
+ calls.push(message);
+ listeners.find(x=>x.type==='message').handler({source:parent,
+ data:{type:'framey:reply',id:message.id,result:true}});
+}};
const context=vm.createContext({window,document:{getElementById:()=>({})},
- navigator:{language:'en'},parent:{postMessage(){}},addEventListener(){},
+ navigator:{language:'en'},parent,addEventListener:(type,handler)=>listeners.push({type,handler}),
setTimeout:()=>1,clearTimeout(){}});
vm.runInContext(host,context);
Object.assign(context,{SP_REACT:window.SP_REACT,SP_JSX:window.SP_JSX,DFL:window.DFL});
@@ -23,3 +31,8 @@ const content=plugin.content.type();
assert.equal(content.type,window.DFL.PanelSection);
assert.equal(content.props.title,'Framey settings sample');
assert.equal(content.children[0][0].type,window.DFL.PanelSectionRow);
+assert.equal(plugin.settings.type,plugin.content.type);
+assert.equal(plugin.frameyQuickActions[0].label,'Read saved setting');
+assert.equal(await plugin.frameyQuickActions[0].onClick(),'Saved setting is on');
+assert.equal(calls[0].method,'get_setting');
+assert.deepEqual(Array.from(calls[0].args),['enabled',false]);