-
Notifications
You must be signed in to change notification settings - Fork 41
example window action
Experimental — see
javascript-reference.md.
The ⋯ menu in every window's title bar is where infrequent, wordy,
per-window verbs live — "Open in browser tab", "Open on startup",
"Reload". wp.os.registerWindowAction() lets your plugin put a row
there too.
Reach for a title-bar button (registerTitleBarButton) instead when
the user will want it constantly. The menu is for the things that would
be clutter as a permanent icon.
wp.os.ready( () => {
wp.os.registerWindowAction( {
id: 'my-plugin/copy-link',
label: 'Copy link to this screen',
icon: 'dashicons-admin-links',
onSelect: ( win ) => {
navigator.clipboard.writeText( win.getCurrentUrl() );
wp.os.showToast( { message: 'Link copied' } );
},
owner: 'my-plugin-shell',
} );
} );owner is the WordPress script handle. Set it and the row disappears
by itself when your plugin is deactivated, with no reload.
label, icon and isVisible may each be a function of the window,
and they are re-read every time the menu opens. That is what lets a
single row say what it will actually do right now:
wp.os.registerWindowAction( {
id: 'my-plugin/pin',
label: ( win ) => ( isPinned( win.id ) ? 'Unpin from top' : 'Pin to top' ),
icon: ( win ) => ( isPinned( win.id ) ? 'dashicons-unlock' : 'dashicons-sticky' ),
onSelect: ( win ) => togglePin( win.id ),
owner: 'my-plugin-shell',
} );Two rows — "Pin" and "Unpin" — would imply a window could be both at once. One row that answers "what does this do?" describes the situation honestly. This is exactly how the Electron Adapter's "Send to your Mac" row becomes "Bring back into OpenStation".
wp.os.registerWindowAction( {
id: 'my-plugin/lint-page',
label: 'Check this page for issues',
icon: 'dashicons-search',
// Iframe windows only — a native window has no admin page to check.
isVisible: ( win ) => ! win.config.native,
onSelect: ( win ) => runLinter( win.getCurrentUrl() ),
owner: 'my-plugin-shell',
} );isVisible is re-read per open, so a row can appear and disappear with
whatever it depends on — a capability, a connection, the page the window
has navigated to — without your plugin re-registering anything.
A verb runs and the menu closes. A checkbox reports a setting the window either has or does not, and stays open when clicked so the user watches the tick land:
const KEY = 'my-plugin/show-gridlines';
wp.os.registerWindowAction( {
id: 'my-plugin/show-gridlines',
label: 'Show gridlines',
checkable: true,
checked: () => localStorage.getItem( KEY ) === '1',
isVisible: ( win ) => win.id === 'my-plugin-canvas',
onSelect: () => {
const next = localStorage.getItem( KEY ) === '1' ? '0' : '1';
localStorage.setItem( KEY, next );
repaintCanvas();
},
owner: 'my-plugin-shell',
} );checked is asked, never told. It runs on every menu open, so you
persist the value and repaint nothing — and the row cannot disagree
with your plugin for longer than one open, however the value changed
(a second window, a settings panel, a REST response landing late).
This is how the built-in Corkboard's "Show pins" works.
closeOnSelect overrides the defaults in either direction: false on
a verb keeps the menu up, true on a checkbox dismisses it after the
flip.
Checkbox or relabelling verb? Use the relabelling label above
when the two states are two places the window can be — the row names
the move. Use a checkbox when they are one setting: a tick says "there
is a thing here, and it is currently off", which a label reading "Show
gridlines" alone cannot.
order sorts your row against other plugins' rows; the built-in items
always come first. Default is 100.
order: 60, // earlier than most-
The menu closes before
onSelectruns for a verb row, so a handler that opens a dialog or navigates is not competing with a still-painted popover. A checkbox instead flips its tick optimistically and leaves the menu open. -
A throwing resolver or handler is contained. A row whose
labelorisVisiblethrows simply does not appear; acheckedthat throws paints unchecked rather than dropping the row; a handler that throws is logged. The ⋯ menu is shared surface — one plugin's bug must not cost the user their "Reload". -
Registration is validated loudly. A bad
id, a missingonSelect, a non-functionisVisible, orcheckablewithoutcheckedthrows aRegistrationErrornaming the field, at registration time, rather than silently painting nothing.
isVisible is re-read per open, but it is synchronous — it cannot go
and ask something. HOOKS.WINDOW_MENU_OPENED can:
wp.os.hooks.addAction( wp.os.HOOKS.WINDOW_MENU_OPENED, 'my-plugin/probe', () => {
void isCompanionAppRunning().then( ( running ) => {
if ( running ) {
wp.os.registerWindowAction( { /* … */ } );
}
} );
} );An open menu repaints when the registry changes, so a row registered from that callback appears under the user's pointer rather than on their next click. This is how the Electron adapter notices an app that started after the page loaded — no refresh needed.
wp.os.unregisterWindowAction( 'my-plugin/pin' );And to see what is registered:
wp.os.listWindowActions(); // sorted by `order`Registering from JS is enough to get the row on screen. To have it leave on deactivation — without the user reloading the page — declare your script server-side and tag each action with the same handle:
add_action( 'admin_enqueue_scripts', function () {
wp_register_script(
'my-plugin-window-actions',
plugins_url( 'js/window-actions.js', __FILE__ ),
array( 'openstation' ),
'1.0.0',
true
);
wp_enqueue_script( 'my-plugin-window-actions' );
} );
openstation_register_window_action_script( 'my-plugin-window-actions' );wp.os.registerWindowAction( {
id: 'my-plugin/pin',
label: 'Pin to top',
onSelect: ( win ) => pin( win.id ),
owner: 'my-plugin-window-actions', // same handle
} );Now the handle rides in the live-refresh payload the shell diffs:
activating your plugin loads the script and the row appears in the next
menu that opens, and deactivating it sweeps out every action carrying
that owner.
Skip the PHP call and owner has nothing to match against — the row
stays until the next page reload. Harmless, and the reason a plugin
written before this existed still behaves.
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config