A WordPress plugin that reimagines /wp-admin as a desktop operating system. Admin screens open as draggable, resizable, minimizable windows on a desktop, with a dock built from the admin menu. Purely opt-in per user — the classic admin stays untouched for everyone else, and deactivating the plugin restores vanilla Core exactly.
Zero Core patches. Every feature is wired through public WordPress hooks.
Demo.mov
Paste this into your coding agent (Claude Code, Cursor, Codex…):
Fork https://github.com/WordPress/openstation and clone my fork. Then:
1. Make sure Docker is running and Node 20+ is installed.
2. In the repo, run: npm install && npm run build
3. Start WordPress with npm run env:start. It uses port 8890; if that port is taken, pick a free one and run WP_ENV_PORT=<port> npm run env:start instead.
4. Check that http://localhost:<port>/wp-admin loads (admin / password) and that the Desktop Mode plugin is active.
5. Run npm test to check the baseline.
6. Read AGENTS.md, CLAUDE.md and docs/, summarise the architecture in five lines, then wait for my first task.
If a step fails, fix it and keep going. Only ask me if you're blocked.
Anyone can build their own plugin on top of OpenStation, in their own folder and repository, without touching OpenStation's code. Install the openstation-extension skill in your plugin's folder:
npx skills add WordPress/openstation --skill openstation-extensionThen ask your agent: Use the openstation-extension skill to build <describe your extension>. The skill sets up a wp-env test site with OpenStation and keeps the agent to its documented hooks, wp.os.* API and <os-*> components.
No skills support in your agent? Paste this instead:
Read https://github.com/WordPress/openstation/blob/trunk/skills/openstation-extension/SKILL.md and follow it to build <describe your extension>.
- Demo
- Develop OpenStation with an AI agent
- Build your own OpenStation extension with an AI agent
- Current State
- Still ahead
- Repository layout
- Bundled extensions
- How to run it
- Requirements
- For plugin authors
- License
-
Station Home Native, role-aware replacement for the ordinary WordPress Dashboard inside OpenStation: recent personal work, four live site instruments, an actionable attention queue, and quick paths to create, upload, view the site, open WP Explorer, or deliberately return to the Classic Dashboard.
-
Per-user opt-in Admin-bar toggle sets the
desktop_mode_modeuser meta. A dedicated/openstation/portal URL auto-enables OpenStation for first-time visitors (gated byopenstation_portal_auto_enable) and theadmin_initredirect sends opted-in users from/wp-admin/to the portal (openstation_admin_redirect_to_portal). -
Desktop shell Fixed-viewport desktop that overlays
/wp-admin: wallpaper area, unified dock (placement picked in OpenStation Preferences — left / right / bottom, default bottom), right-column widget layer, and full windowing system.openstation_mode_init,openstation_shell_before/_after, and theopenstation_shell_configfilter are the main extension points. -
Window system — iframe + native Iframe windows load admin pages with
?openstation_chromeless=1(chromeless mode). Native windows render directly in the parent DOM viaopenstation_register_window()/wp.os.registerWindow()— multi-tab native windows are supported throughopenstation_register_window_tab(). Both types share drag, resize, minimize, maximize, close, fullscreen, and detach-to-new-tab. -
Dock One unified rail hosting every admin menu — core and plugin alike — plus shell-level system tiles. Placement (left / right / bottom) is a user preference in OpenStation Preferences. Core menus are ordered before plugin menus; per-item hiding via
openstation_dock_placement('hidden'). Per-item multi-window support viaopenstation_dock_item_multi. Letter-badge icon fallback for plugins without icon art. The Dynamic dock behavior (auto-hide) folds the rail into a thin line at its edge and morphs it back with the View Transitions API. -
Virtual desktops (“Spaces”) Multiple desktops per user, each with its own window set. The Workspaces grid (zoom-out view) surfaces the Spaces switcher, thumbnails, and create/close controls.
-
Arrange & snap Workspaces from its dock tile;
cascade(),tile()andsetSnapEnabled()onwp.os.windowManager. Tile grid dimensions and snap cell size are both filterable. -
Wallpaper registry Server- and client-side registration (
openstation_register_wallpaper()/wp.os.registerWallpaper()). CSS presets + canvas (WebGL/2D) wallpapers with collision-aware surface data (wp.os.getWallpaperSurfaces()) for snow/rain/physics effects. In-panelrenderEditorcallback for custom controls, shared vendor-module loader (pixijspre-registered). -
Widgets Right-column floating cards, optionally draggable / resizable outside the column.
openstation_register_widget()/wp.os.registerWidget(). Built-in clock. User placement persists per-user inlocalStorage. -
Desktop icons Wallpaper-layer shortcuts via
openstation_register_icon()— targets a registered native window or an admin URL. -
AI Assistant + slash commands Cmd+K palette backed by an agentic loop on the WordPress AI Client (WordPress 7.0+): provider credentials come from Core's Connectors screen, and the model's tools are read-only abilities (
search_posts/search_pages/search_commentsrun WordPress's native keyword search). No content is analyzed in the background: every AI call is one a user asked for.wp.os.registerCommand()adds slash commands with autocomplete (suggest()), confirm dialogs (ctx.confirm()), and full lifecycle hooks (before-run/after-run/error). Built-in/open [window]is extensible viaos.open-command.items. -
Palette registry Cmd+K cycles through all registered palettes (
wp.os.registerPalette()) — the AI assistant is palette 0 by default; additional plugin overlays share the shortcut. -
Cross-window drag & drop into Gutenberg Media Library attachments and My WordPress media, post and user tiles drag across iframe boundaries via coordinated postMessage. Dropped on a post editor window, they become blocks:
core/image/core/video/core/audio/core/fileby mime type, or a linked paragraph for a post or user, inserted where Gutenberg's own insertion line shows under the pointer. A desktop file is copied into the Media Library on the way in. The Media Library side has a site-wide toggle in Extended Options. Seedocs/bridge-protocol.md. -
Toast notifications Shell-level toasts rendered via the
<os-toast>component. Plugins register their own tone/icon via theopenstation_toast_typesfilter. Iframe pages raise a toast through theos-notificationbridge message — it survives the iframe's own lifecycle. -
OpenStation Preferences The settings window, an App Framework app (
apps/os-settings/): wallpaper picker (with HD-only media filter), accent color swatches + custom gradient editor, desktop layout and dock controls, themes, window effects, navigation placement, feature switches and the component reference. Persisted via/desktop-mode/v1/os-settings. -
Session persistence Full window stack (including desktops, focus, state) is debounce-saved to
/desktop-mode/v1/sessionand restored without layout flicker. Viewport-shrink clamping keeps off-screen windows reachable. -
Mobile — the phone layer Below 768px the same window manager renders a phone: a home screen of the navigation's tiles with search, one full-screen app at a time under a slim top bar, a swipeable app switcher, a five-slot tab bar, edge-swipe Back.
wp.os.modereportsdesktop | tablet | mobile; a head stamp makes the first paint right; a phone restores one window and hands the desktop its session back untouched. Lazy bundle, wallpaper suspended, widgets skipped. Seedocs/mobile.md. -
postMessage bridge Typed messages for title changes, navigation (same-origin validated), focus, color-scheme sync, screen-meta panels (Screen Options / Help), external-link capture, iframe-ready handshake, and observability (
iframe-error,iframe-network). -
UI component library ~70
<os-*>web components (os-button,os-menu,os-panel,os-range-field,os-swatch,os-toast,os-tabs, …) available to plugin authors — rendered server-side viaopenstation_component()or imported in TS. -
Design tokens & desktop themes Every shell surface reads its colours from
--os-*tokens inassets/css/variables.css. Desktop themes (openstation_register_desktop_theme()or an uploaded ZIP) re-skin them wholesale, and the built-in Legacy theme restores the pre-brand look. Seedocs/desktop-themes.md. -
i18n Full gettext coverage across PHP and TypeScript; Spanish translation shipped. Strings go through
wp.i18n(__,_x,_n,sprintf) directly — no shell-specific re-export. -
Component registration API Stable
openstation_register_*functions for windows, widgets, wallpapers, icons, and window tabs. All returntrue/WP_Errorwith documented error codes. -
Public hook API Comprehensive PHP and JS hook surface — dock items, placement, multi-window, native-window lifecycle, widget lifecycle, wallpaper lifecycle + surfaces, window lifecycle, iframe observability, arrange actions, virtual-desktop transitions, palette registration, command lifecycle, batch close, AI prompt + model + tool filters, accents, toast types, default wallpaper. See
docs/hooks-reference.mdanddocs/javascript-reference.md.
- Mobile, the next pass — pull-to-refresh, which needs the chromeless bridge to report overscroll from inside the iframe.
- Tablet hybrid — split view and slide-over, on the
wp.os.modeprimitive that already reports'tablet'. A tablet gets the desktop today. - Cross-window drag & drop, the next pass — pluggable mime-type negotiation (the payload-to-block map is fixed today, and the planned
openstation_drag_*filters don't exist yet), and a drop receiver for the Site Editor. - Polish — full a11y audit (ARIA, focus traps, keyboard nav).
- …and a whole lot more hooks, filters, and actions — every new surface lands with its own extension points, so this list keeps growing.
See docs/architecture.md for how the pieces fit together and docs/hooks-reference.md for the hook surface (current and planned).
.
├── desktop-mode.php # bootstrap: header, constants, require_once of includes/
├── includes/ # PHP subsystems
│ ├── helpers.php admin-bar.php ajax.php
│ ├── assets.php render.php portal.php
│ ├── session.php default-window.php components.php
│ ├── os-settings.php extended-options.php
│ ├── accents.php wallpapers.php toast-types.php
│ ├── media-query.php
│ └── ai-copilot/ # AI assistant (WordPress AI Client adapter, abilities, search, on-demand comment analysis)
├── apps/ # App Framework apps: <name>.os.php (window, state, actions,
│ # data) + optional <name>.os.ts (client view) + <name>.css;
│ # Code Blue lives here — see docs/app-framework.md
├── assets/ # hand-authored CSS + JS build output
│ ├── css/ desktop.css, windows.css, dock.css, chromeless.css, variables.css
│ │ variables.css carries the OpenStation palette — every design
│ │ token the shell and the <os-*> kit read, declared once
│ ├── fonts/ Geist + Geist Mono (variable, SIL OFL 1.1)
│ ├── wallpapers/ the four brand desks: galaxy (default), space,
│ │ holomesh, pulsemesh
│ ├── desktop-themes/legacy/
│ │ the built-in "Desktop Mode (Legacy)" theme — a frozen snapshot of
│ │ every token at its pre-brand value; never regenerated.
│ │ Zip it with npm run package:legacy-theme
│ └── js/ Vite bundles (gitignored; regenerate with npm run build) — only
│ admin-bar.js and media-library-enhanced.js are hand-written and tracked
├── src/ # TypeScript source — compiled by Vite
│ ├── desktop.ts / dock.ts / hooks.ts / commands.ts / palette-registry.ts
│ ├── ai-assistant/ + drag-bridge.ts / toast.ts / desktop-icons.ts
│ ├── native-windows.ts / built-in-commands.ts / public-api.ts / types.ts
│ ├── window/ # Window class — DOM, pointer, tabs, iframe bridge
│ ├── window-manager/ # stack, desktops, arrange, snap, overview
│ ├── wallpapers/ # registry, layer, surfaces, server sync, vendor loader
│ ├── widgets/ # registry, layer, frame, picker, storage
│ ├── settings/ # settings store + registry (the window is apps/os-settings/)
│ ├── ui/ # <os-*> web components
│ ├── modules/ # vendor-script lazy-loader
│ └── plugins/ # built-in widgets and wallpapers, one bundle each
├── docs/ # developer-facing docs (source of truth for plugin authors)
├── extensions/ # bundled sibling plugins (see "Bundled extensions" below)
├── tests/ # PHPUnit + Vitest
├── languages/ # .po / .mo (es shipped)
├── bin/ # package-zip helpers
├── package.json # devDeps (vite, typescript, vitest)
├── vite.config.js # Vite lib-mode: src/desktop.ts → assets/js/desktop[.min].js (IIFE)
├── vitest.config.ts
└── tsconfig.json
The extensions/ directory hosts sibling plugins that build on OpenStation's public APIs. Each one is a standalone WordPress plugin (Requires Plugins: desktop-mode) installed from its own zip — run ./bin/package-extensions.sh to build one <slug>.zip per extension under dist/; see docs/RELEASE.md for the full packaging steps. (extensions/base/ is a shared base library for extension authors, and extensions/mio-js/ is a standalone build of Mio for any web page; neither is an installable plugin.)
Extensions that are not bundled here, such as the Code Editor (desktop-mode-code-editor) and the Cron Manager (desktop-mode-cron-manager), live in desktop-mode-official-extensions.
- Electron Adapter (
openstation-electron-adapter) — lets any OpenStation window leave the desk as a real OS window when the desktop is opened through the bundled OpenStation Desktop (Electron) app. Adds nothing to the browser experience. - OpenStation Beta (
openstation-beta) — switches the installed OpenStation to a pull-request build, the trunk build, or back to the latest stable release. Never distributed on WordPress.org. - Popup Siege (
desktop-mode-popup-siege) — a 90-second Breakout-style archive rescue game with OpenStation leaderboards, play-time tracking, and score-to-beat challenges. The deterministic game runtime is lazy-loaded on first play. - SOL Inbound Monologue (
desktop-mode-feed-buddy) — an AIM-era RSS/Atom reader with a movable buddy-list widget, a native conversation-style reader window, per-user subscriptions and unread state, safe server-side feed discovery, and optional synthesized chimes.
Just want to try it? Grab the pre-built zip and upload it to any WordPress — Studio by WordPress.com, wp-env, or a hosted site. No Node, no build step.
- Download
openstation.zipfrom the latest release (or pick a specific version from the releases page). - In WP Admin: Plugins → Add New → Upload Plugin, choose the zip, and activate.
- Click the desktop icon in the admin bar's top-right corner. The admin reloads inside the desktop shell. Click the same icon again to return to classic admin.
For hacking on the plugin: clone the repo, run the build in watch mode, and load it into a local WordPress via symlink so every save is one browser refresh away.
npm installThe plugin uses Vite in library mode. esbuild handles transpile and minify, so builds finish in ~70 ms per bundle.
Full build — runs the PixiJS vendor-copy step plus every build:* target defined in package.json (one Vite bundle each; the scripts block in package.json and the entry map in vite.config.js are the authoritative target list):
npm run buildWrites one assets/js/<target>.js / .min.js pair per target, including:
assets/js/desktop.js/.min.js— main shell bundle (loaded based onSCRIPT_DEBUG).assets/js/iframe-bridge.js/.min.js— opt-in bridge that gives any same-origin iframe access towp.os.iframe.*.assets/js/apps/<name>.js/.min.js— App Framework client views, built fromapps/<dir>/<name>.os.tsbynpm run build:apps. The native Posts window (apps/posts/, the<os-table>replacement for theedit.phpiframe, opt-in per user via OpenStation Preferences → Features) is one of them.
Development watch — auto-recompiles the unminified bundle on save:
npm run devLeave it running in a separate terminal; refresh the browser after each save. Set define( 'SCRIPT_DEBUG', true ) in wp-config.php so WordPress picks up the unminified bundle during development.
You need a running WordPress to load the plugin into. Pick whichever is easier.
Run npm run package to build a zip from HEAD (with correct 0644 / 0755 permissions), then follow the Quick install steps 2–3 to upload and activate it. Re-package and re-upload after each change.
If you changed source, run
npm run buildbeforenpm run package— the Vite output is gitignored, andbin/package.shsplices the built files into the zip from your working tree.
npm run package:legacy-themebuildsdist/desktop-mode-legacy-theme.zip— the built-in Legacy desktop theme as an installable theme ZIP. The plugin registers Legacy from code either way; the zip is for handing it to someone else or forking it into a theme of your own.
Gives you the full dev loop: npm run dev rebuilds on save, a browser refresh picks it up.
# clone Core's Docker-based dev host alongside this repo
git clone https://github.com/WordPress/wordpress-develop.git
cd wordpress-develop
npm install
# symlink this plugin into the WP plugins directory
ln -s "$(pwd)/../alcazaba-plugin" src/wp-content/plugins/desktop-mode
# boot + install WordPress
npm run env:start # nginx + PHP + MySQL in Docker
npm run env:install # installs WordPressSite: http://localhost:8889
Admin: http://localhost:8889/wp-admin/
Credentials: admin / password
Stop the environment with npm run env:stop (from the wordpress-develop directory). Activate the plugin per Quick install steps 2–3.
- WordPress 6.0+
- PHP 7.4+
This plugin is built to be extended. Every significant behavior is hookable — drop an icon on the desktop, add a dock item, gate OpenStation by role, react to window events, or register a native window, all from your own plugin with zero patches here.
See docs/ — the developer documentation index.
Quick links:
- Getting Started — the five-minute tour for plugin authors.
- Architecture — how the pieces fit together.
- Hooks Reference — every action and filter we fire, with signatures and examples.
- JavaScript Reference — CustomEvents,
window.wp.osAPI, and the iframepostMessagebridge. - Examples — copy-paste recipes.
Found a vulnerability? Please email security@automattic.com rather than opening an issue. See SECURITY.md.
GPLv2 or later. See LICENSE.