-
Notifications
You must be signed in to change notification settings - Fork 41
migration shell screen
Who this affects: anything that keyed shell behaviour on the admin
screen the shell happened to be painted over — $pagenow === 'index.php',
get_current_screen()->id === 'dashboard', a load-index.php hook, or a
script enqueued "on the Dashboard" that only reached the desktop because
the desktop was the Dashboard. Also anything that built a desktop URL
by hand as wp-admin/index.php?desktop_mode_portal=1.
What to do: gate on openstation_is_shell_request() instead of a
screen id, and build desktop URLs with openstation_shell_url() (or
openstation_portal_url()). Nothing else changes: every
openstation_register_* call, the payload shape, wp.os.*, the events
and openStationConfig are untouched.
The desktop shell used to be injected on top of whichever admin screen
the /openstation/ portal forwarded to: the Dashboard by default, the
last-focused window's URL otherwise, edit.php or post.php if that is
where the user had been. The shell document therefore inherited that
screen's whole script and style queue, its server-side render, and its
hidden HTML — and then loaded the same screen again inside a window.
On a site running the Gutenberg plugin the cost was the whole editor
closure: Gutenberg's experimental Dashboard page enqueues its loader on
index.php, the loader depends on wp-editor, and since the shell
document was index.php, 162 requests / 20 MB raw printed, parsed and
executed in the shell's realm where nothing ever rendered them.
The shell now boots from a screen OpenStation owns:
admin.php?page=openstation[&target=<admin path>][&intent=1]
It is registered under an empty parent, so it appears in no menu, and
its capability is read — the same floor the portal applies. Its
screen id is admin_page_openstation; $hook_suffix on
admin_enqueue_scripts is the same string.
is_admin() is true there and admin_menu / admin_enqueue_scripts
fire exactly as before, which is why it is an admin page rather than a
standalone document: those hooks are the contract behind every
openstation_register_* call and every plugin that gates registration
on is_admin() at load time.
| Request | Before | Now |
|---|---|---|
/openstation/ |
302 → index.php?desktop_mode_portal=1 (or the focused window's URL) |
302 → admin.php?page=openstation; the screen resolves the entry itself |
/openstation/?target=/wp-admin/edit.php |
302 → edit.php?desktop_mode_portal=1&desktop_mode_portal_intent=1
|
302 → admin.php?page=openstation&target=%2Fwp-admin%2Fedit.php&intent=1
|
/wp-admin/edit.php (plain GET, user enabled) |
rendered in place with the shell over it | 302 → the shell screen with target=/wp-admin/edit.php&intent=1 (one hop) |
/wp-admin/network/sites.php (not allowlisted) |
302 → /openstation/?target=… → focused window |
unchanged, ending on the shell screen |
index.php?desktop_mode_portal=1 (old bookmark, PWA start URL) |
rendered in place | 302 → the shell screen with that URL as target
|
desktop_mode_portal and desktop_mode_portal_intent stay frozen (see
AGENTS.md). They are aliases now: a URL carrying them is the desktop's
pre-screen address and redirects to the screen. Nothing that emits them
breaks; nothing new should emit them.
currentPage, fromPortal and fromPortalIntent in openStationConfig
keep their meaning. currentPage is the validated target (else the
session's focused window, the default window, the Dashboard);
fromPortal is true on the screen by construction; fromPortalIntent
is the intent arg. The auto-open matrix in src/boot/auto-open.ts is
unchanged.
A script that only reached the desktop because the desktop was the
Dashboard. Something enqueued when $hook_suffix === 'index.php', or
inside load-index.php, or behind get_current_screen()->id === 'dashboard', used to print in the shell document. It now prints in the
Dashboard window and nowhere else — which is where it was always
meant to run. If it genuinely needs to run in the shell's own realm
(it publishes a window.openStation* global the shell reads, say),
enqueue it when openstation_is_shell_request() is true.
A hand-built desktop URL. admin_url( 'index.php?desktop_mode_portal=1' )
still works through the alias, at the cost of one redirect. Build the
URL with openstation_shell_url() (bare, or with a target and an
intent flag) or link to openstation_portal_url().
openstation_admin_redirect_to_portal returning false. That filter
used to keep the shell on the page the user asked for; it now keeps the
user on a classic page, because the shell has no way to render there
any more. With the redirect disabled the desktop lives at
/openstation/ and its screen only. The frozen-flag alias still
redirects: a URL naming the desktop is not a plain admin page.
$pagenow / get_current_screen() inside a shell-only hook. On a
shell boot they now report admin.php and admin_page_openstation.
Inside windows (chromeless requests) they report the window's page, as
always.
-
openstation_is_shell_request()— true when this request paints the shell: the shell screen for a user with OpenStation enabled, or a solo boot (?openstation_solo=<id>, which renders in place). Never inside a window, never on a classic-flagged request. Every render hook that used to spell out "enabled, not chromeless, not classic" reads this now. -
openstation_is_shell_screen_request()— true on the shell screen regardless of whether the shell renders there (a disabled user, a window, a classic-flagged request can all address it). -
openstation_url_is_shell_screen( $url )— whether a URL names the screen. The shell never opens itself: the portal sanitiser, the session entry resolver, the iframe URL builder, session restore, hover prewarming and the service worker's speculative documents all refuse it.
With no host screen, the shell document carries OpenStation's own assets, Core's every-admin-page set, and whatever plugins enqueue on every admin page. The framework does not guess which of the last group belongs in the shell; the site says so:
add_filter( 'openstation_shell_dequeue_handles', function ( $handles, $kind ) {
if ( 'script' === $kind ) {
$handles[] = 'acme-upsell-nag';
}
return $handles;
}, 10, 2 );A handle that a surviving script or style still depends on is refused
with a _doing_it_wrong() rather than dropped. See
hooks-reference.md.
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 — the shell boots from its own screen
- Migration: a native window's tabs move to the window chrome
- Migration — WordPress package globals are no longer ambient
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
- Place something where the user can reach it — wp.os.workArea