-
Notifications
You must be signed in to change notification settings - Fork 41
example plugins window extras
Recipes for plugin authors that want to extend the native Plugins window. Every PHP hook listed below is documented in docs/hooks-reference.md; JS-side surface lives in docs/javascript-reference.md.
Add a "Curated" tab to the Browse segmented filter that calls plugins_api( 'query_plugins' ) with a fixed wp.org tag.
<?php
add_filter(
'openstation_plugins_window_browse_args',
static function ( array $api_args, array $raw ): array {
if ( 'curated' !== ( $raw['browse'] ?? '' ) ) {
return $api_args;
}
// Override the upstream call with our pinned tag.
unset( $api_args['browse'] );
$api_args['tag'] = 'gutenberg-block';
return $api_args;
},
10,
2
);JS side — Planned, not yet implemented: the openstation.pluginsWindow.browseFilters filter below does not exist yet (the Browse segments are currently hard-coded in the bundle). The intended shape, once the JS filter registry lands:
// Planned — not yet implemented.
addFilter(
'openstation.pluginsWindow.browseFilters',
'my-plugin/curated',
( filters ) => [
...filters,
{ value: 'curated', label: __( 'Curated', 'my-plugin' ) },
]
);Until the JS filter registry lands, you can also subclass the segmented control or layer your own segment via the
openstation_plugins_window_template_htmlfilter.
The icon resolver tries three things in order:
-
Local file in the plugin folder. If your plugin ships
assets/icon.svg(orassets/icon-256x256.png,assets/icon-128x128.png, or the same names at the folder root), the resolver picks it automatically — no PHP wiring needed. Mirror the wp.org SVN /assets/ layout and you're done. This is the recommended path for premium / internal / native-bundled plugins that aren't on the .org repo. -
The
iconsmap wp.org returned for the plugin, cached in theupdate_pluginstransient and readsvg→2x→1x, the way core's Add Plugins cards read it. Directory-listed plugins land here and need nothing from you. -
Guessed wp.org SVN asset —
https://ps.w.org/<slug>/assets/icon.svg, for when that metadata isn't cached yet.
For a non-standard convention (e.g. you ship branding/logo.svg), extend the candidate list rather than overriding the final URL:
<?php
add_filter(
'openstation_plugins_window_local_icon_candidates',
static function ( $candidates ) {
$candidates[] = 'branding/logo.svg';
return $candidates;
}
);To force a specific URL — e.g. a CDN-hosted icon for a premium plugin that doesn't ship art with the bundle — use the openstation_plugins_window_icon_url filter instead:
<?php
add_filter(
'openstation_plugins_window_icon_url',
static function ( $url, string $slug, array $row ) {
if ( 'my-premium-plugin' === $slug ) {
return 'https://cdn.example.com/icons/my-premium-plugin@2x.png';
}
return $url;
},
10,
3
);Returning null from openstation_plugins_window_icon_url suppresses the icon entirely (forces the placeholder).
The default reviews handler hits https://wordpress.org/plugins/{slug}/#reviews and parses the HTML with DOMDocument. wp.org HTML can change, so the result is best-effort and falls back to a histogram-only view on parse failure.
If you maintain a more robust parser (or have access to a private reviews API), short-circuit the default by returning an array of items:
<?php
add_filter(
'openstation_plugins_window_review_parser',
static function ( $items, string $slug ) {
if ( null !== $items ) {
return $items; // Already overridden upstream.
}
$cached = get_transient( 'my_plugin_reviews_' . $slug );
if ( is_array( $cached ) ) {
return $cached;
}
// …call your own reviews API…
$rows = my_plugin_fetch_reviews( $slug );
return array_map(
static fn( $r ) => array(
'author' => (string) $r->author,
'stars' => (int) $r->stars, // 1–5
'excerpt' => (string) $r->excerpt,
'date' => (string) $r->date, // free-form, e.g. "August 2026"
'url' => (string) $r->permalink,
),
$rows
);
},
10,
2
);Return null to fall through to the default DOMDocument parser.
Hook the openstation_plugins_window_installed action to seed defaults when a new plugin lands via the upload route:
<?php
add_action(
'openstation_plugins_window_installed',
static function ( string $plugin_file ): void {
// $plugin_file is e.g. "akismet/akismet.php"
if ( 'my-plugin/my-plugin.php' === $plugin_file ) {
update_option( 'my_plugin_first_install_at', time() );
}
}
);This action only fires for the wp_ajax_openstation_plugins_upload route. Installs that go through Core's wp_ajax_install_plugin (the slug-based path) trigger Core's own upgrader_process_complete action — wire to that for cross-source coverage.
The bundle reads an initial-tab hint from a shared store. Set it BEFORE openById( 'desktop-mode-plugins' ):
import { setPluginsWindowTab } from 'openstation/plugins-window/tab-target';
const myButton = document.querySelector( '#explore-plugins' )!;
myButton.addEventListener( 'click', () => {
setPluginsWindowTab( 'browse' );
window.wp.os.openWindow( 'desktop-mode-plugins' );
} );Backed by wp.os.createSharedStore so multiple bundles read the same value. The hint is consumed (cleared) on first read by the render callback, so a subsequent open without an explicit hint defaults back to "installed".
Cards in the Browse gallery emit a wporg-plugin payload via the framework drag bridge. Register your own drop target so plugin authors can drag a card into your custom canvas:
window.wp.os.dragManager.registerDropTarget( {
id: 'my-plugin/canvas',
element: document.querySelector( '#my-canvas' )!,
accept: ( payload ) => payload.type === 'wporg-plugin',
onEnter: ( session ) => {
document.querySelector( '#my-canvas' )!
.classList.add( 'is-drop-target' );
},
onLeave: () => {
document.querySelector( '#my-canvas' )!
.classList.remove( 'is-drop-target' );
},
onDrop: ( session, ev ) => {
const { slug, name, iconUrl } = session.payload.data as {
slug: string;
name: string;
iconUrl: string | null;
};
// …attach the plugin to your canvas at (ev.clientX, ev.clientY)…
},
} );The framework's drag bridge handles the ghost element + hit-testing for you; the payload type is the contract — anything matching 'wporg-plugin' is a card from this window.
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