PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/render/assets.php +660 -330 0.9.7 → 1.1.12 View file →
@@ -1,11 +1,11 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Asset enqueue.
3 + * OpenStation — Asset enqueue.
4 4 *
5 - * Loads the desktop shell CSS + JS bundles when desktop mode is
5 + * Loads the desktop shell CSS + JS bundles when OpenStation is
6 6 * active and the request isn't chromeless / classic-overridden.
7 - * Owns the entire `desktop_mode_enqueue_assets()` body — the
7 + * Owns the entire `openstation_enqueue_assets()` body — the
8 8 * largest hook in the original render.php and the natural seam
9 9 * for "what does the shell ship to the browser today?".
10 10 *
11 11 * Extracted from `render.php` during the architecture-0.8.1 PHP
@@ -10,42 +10,39 @@
10 10 *
11 11 * Extracted from `render.php` during the architecture-0.8.1 PHP
12 12 * slicing (phase 6).
13 13 *
14 - * @package Desktop_Mode
15 - * @since 0.8.1
14 + * @package OpenStation
16 15 */
17 16
18 17 defined( 'ABSPATH' ) || exit;
19 18
20 19 /**
21 - * Enqueues the desktop mode shell assets (CSS + JS) when desktop mode is active.
20 + * Enqueues the OpenStation shell assets (CSS + JS) when OpenStation is active.
22 21 *
23 22 * Only loads the full desktop shell scripts and styles when the user has
24 - * desktop mode enabled and the request is not a chromeless iframe load.
25 - *
26 - * @since 0.1.0
23 + * OpenStation enabled and the request is not a chromeless iframe load.
27 24 */
28 -function desktop_mode_enqueue_assets() {
25 +function openstation_enqueue_assets() {
29 26 if ( ! is_admin() ) {
30 27 return;
31 28 }
32 29
33 - // Auto-enqueue the iframe bridge anywhere a desktop-mode user
30 + // Auto-enqueue the iframe bridge anywhere a openstation user
34 31 // might land. The bundle self-bails when not inside an iframe
35 32 // (`window.parent === window`), so it's a no-op on the parent
36 33 // shell — but cheap insurance against the failure mode the
37 34 // developer hit: an internal admin navigation drops the
38 - // `?desktop_mode_chromeless=1` flag, the chromeless inline bridge doesn't
39 - // run, and `wp.desktop.iframe` silently disappears. With this
35 + // `?openstation_chromeless=1` flag, the chromeless inline bridge doesn't
36 + // run, and `wp.os.iframe` silently disappears. With this
40 37 // auto-enqueue, the API is universally present for any same-
41 - // origin admin page a desktop-mode user opens — chromeless or
38 + // origin admin page a openstation user opens — chromeless or
42 39 // accidentally classic.
43 - if ( desktop_mode_is_enabled() ) {
44 - wp_enqueue_script( 'desktop-mode-iframe-bridge' );
40 + if ( openstation_is_enabled() ) {
41 + wp_enqueue_script( 'os-iframe-bridge' );
45 42
46 43 // Block Editor cross-window drop receiver. Listens for
47 - // `desktop-mode-drop` postMessages from the parent shell and
44 + // `os-drop` postMessages from the parent shell and
48 45 // inserts the matching block. Only enqueue inside the
49 46 // post-edit Block Editor screens — every other admin page
50 47 // would be paying for a bundle it never uses.
51 48 //
@@ -56,48 +53,106 @@
56 53 // after the receiver's 5 s `waitForEditor()` poll. Re-enable
57 54 // once we have a reliable readiness signal in that context.
58 55 global $hook_suffix;
59 56 if ( 'post.php' === $hook_suffix || 'post-new.php' === $hook_suffix ) {
60 - wp_enqueue_script( 'desktop-mode-gutenberg-drop-receiver' );
57 + wp_enqueue_script( 'os-gutenberg-drop-receiver' );
61 58 }
62 59 }
63 60
64 61 // Chromeless requests (iframes) need chromeless styles and overrides.
65 - if ( desktop_mode_is_chromeless_request() ) {
66 - wp_enqueue_style( 'desktop-mode' );
67 - wp_enqueue_style( 'desktop-mode-chromeless' );
62 + if ( openstation_is_chromeless_request() ) {
63 + wp_enqueue_style( 'openstation' );
64 + wp_enqueue_style( 'os-chromeless' );
68 65
69 66 /**
70 - * Fires when chromeless styles are enqueued inside a desktop mode iframe.
67 + * Fires when chromeless styles are enqueued inside a OpenStation iframe.
71 68 *
72 69 * Plugin and theme authors can hook here to enqueue their own CSS
73 70 * overrides for legacy pages rendered in chromeless mode. Use the
74 - * `.desktop-mode-chromeless` body class to scope your rules.
75 - *
76 - * @since 0.1.0
71 + * `.os-chromeless` body class to scope your rules.
77 72 */
78 - do_action( 'desktop_mode_chromeless_styles' );
73 + do_action( 'openstation_chromeless_styles' );
79 74 return;
80 75 }
81 76
82 - if ( ! desktop_mode_is_enabled() || desktop_mode_is_classic_request() ) {
77 + if ( ! openstation_is_shell_request() ) {
83 78 return;
84 79 }
85 80
86 - // CSS.
87 - wp_enqueue_style( 'desktop-mode' );
88 - wp_enqueue_style( 'desktop-mode-windows' );
89 - wp_enqueue_style( 'desktop-mode-window-overview' );
90 - wp_enqueue_style( 'desktop-mode-os-settings' );
91 - wp_enqueue_style( 'desktop-mode-dock' );
92 - wp_enqueue_style( 'desktop-mode-dock-peek' );
93 - wp_enqueue_style( 'desktop-mode-ai-assistant' );
94 - wp_enqueue_style( 'desktop-mode-bug-report' );
95 - wp_enqueue_style( 'desktop-mode-files' );
96 - wp_enqueue_style( 'desktop-mode-notes' );
81 + // CSS. Only the sheets that paint surfaces present at boot — the
82 + // shell chrome, the dock, desktop tiles and pinned notes. Sheets
83 + // for on-demand surfaces (Preferences panel, AI assistant, bug
84 + // report) ship as `deferredStyles` in the config blob below and
85 + // inject on first open; a native window's sheet rides its
86 + // registration's `styles` companion list the same way.
87 + wp_enqueue_style( 'openstation' );
88 + wp_enqueue_style( 'os-windows' );
89 + wp_enqueue_style( 'os-window-overview' );
90 + wp_enqueue_style( 'os-dock' );
91 + wp_enqueue_style( 'os-dock-peek' );
92 + wp_enqueue_style( 'os-workspaces' );
93 + wp_enqueue_style( 'os-shortcuts' );
94 + wp_enqueue_style( 'os-openstation-layout' );
95 + wp_enqueue_style( 'os-files' );
96 + wp_enqueue_style( 'os-notes' );
97 + // Unconditional like the layout sheet: a live crossing into the
98 + // phone band must not find the phone layer unstyled.
99 + wp_enqueue_style( 'os-mobile' );
97 100
101 + // Solo mode — a single window freed into a native OS window by the
102 + // desktop host. Same shell, everything but that one window hidden.
103 + $solo_window = openstation_solo_window_id();
104 + if ( '' !== $solo_window ) {
105 + wp_enqueue_style( 'os-solo' );
106 +
107 + /*
108 + * Hide every window that is not the one this surface was booted
109 + * to paint — from the first frame, before any of them exist.
110 + *
111 + * Solo mode promises one window. Anything that opens a second
112 + * (a game launched from a freed Games hub, a plugin calling
113 + * `openWindow`) would otherwise land on top of the first, and
114 + * solo's CSS stretches every window to fill the viewport, so it
115 + * covers what the user was using.
116 + *
117 + * This has to be CSS rather than JavaScript, and it has to be
118 + * inline. A JS rule can only run once the window exists, which
119 + * is a frame too late — the user sees the newcomer flash before
120 + * it is dealt with. A static stylesheet cannot express it
121 + * either, because the selector depends on which window this is.
122 + * So the rule is emitted with the id baked in, and no window but
123 + * that one is ever painted.
124 + *
125 + * `visibility` rather than `display`: a hidden-but-laid-out
126 + * window still has a size, which canvas-based windows need in
127 + * order to initialise without dividing by zero on the way to
128 + * being closed.
129 + *
130 + * The id is `sanitize_key()`-clean (see `openstation_solo_window_id()`),
131 + * so it is safe in a selector; it is escaped again here because
132 + * the distance between those two facts is exactly where this
133 + * kind of bug lives.
134 + */
135 + wp_add_inline_style(
136 + 'os-solo',
137 + sprintf(
138 + 'body.os-solo .os-window:not(#wp-window-%1$s){visibility:hidden !important;pointer-events:none !important;}',
139 + esc_attr( $solo_window )
140 + )
141 + );
142 + }
143 +
144 + // The rebrand announcement paints on one visit per user and never
145 + // again, so its stylesheet is only worth sending to the users who
146 + // are actually going to see it. Computed once here and reused for
147 + // the `rebrandNotice` config key below, which reads the same answer.
148 + $show_rebrand_notice = openstation_should_show_rebrand_notice();
149 + if ( $show_rebrand_notice ) {
150 + wp_enqueue_style( 'os-announce' );
151 + }
152 +
98 153 // JS.
99 - wp_enqueue_script( 'desktop-mode' );
154 + wp_enqueue_script( 'openstation' );
100 155
101 156 // `wp_enqueue_command_palette_assets()` (WP 6.9+) enqueues the
102 157 // `wp-commands` store package, the `wp-core-commands` script that
103 158 // registers the WordPress-wide baseline (Add new post, Manage
@@ -111,43 +166,45 @@
111 166 // WP normally only calls this on screens that opt in to the native
112 167 // palette; the shell needs it on every admin URL it might wrap.
113 168 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
114 169 // no-ops when the store is missing.
170 + // See `openstation_defer_core_command_palette()` below for why
171 + // Core's own boot-time enqueue is unhooked on shell pages.
172 + //
173 + // The Core command-palette runtime is NOT enqueued here any more.
174 + // Its dependency chain is the whole Gutenberg runtime (~800 KB
175 + // gzipped across forty-odd bundles), paid on every boot for a ⌘K
176 + // palette most sessions never open. It now ships as an ordered
177 + // manifest in the config blob (`commandPalette`, built by
178 + // `openstation_build_command_palette_assets_payload()`), and
179 + // `src/commands/palette-assets.ts` replays it the first time the
180 + // palette is invoked. The shell harvester keeps its idle-time
181 + // `install()` — a graceful no-op until the store exists — and
182 + // re-installs on `os-command-palette-ready`.
183 + $command_palette = openstation_build_command_palette_assets_payload();
184 +
115 185 if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
116 - // `wp_enqueue_command_palette_assets()` calls
117 - // `array_key_exists( $menu_slug, $submenu )` without guarding
118 - // the global, so an unset `$submenu` (test contexts, edge-case
119 - // admin requests where the menu wasn't built yet) blows up
120 - // with a TypeError. Initialize defensively before calling.
121 - global $menu, $submenu;
122 - if ( ! isset( $submenu ) || ! is_array( $submenu ) ) {
123 - $submenu = array();
124 - }
125 - if ( ! isset( $menu ) || ! is_array( $menu ) ) {
126 - $menu = array();
127 - }
128 - wp_enqueue_command_palette_assets();
129 -
130 186 // Expose the same menu-commands array WP serializes into
131 187 // `wp.coreCommands.initializeCommandPalette(...)` on a window
132 188 // slot the shell harvester can read. Built in PHP from `$menu`
133 - // / `$submenu` here (we already guarded that they're arrays
134 - // above), then injected as a `before` inline on our own bundle
135 - // — that runs synchronously before `desktop.min.js` boots the
136 - // shell harvester, so the lookup is guaranteed populated by
137 - // the time `src/commands/shell-harvester.ts` classifies any
138 - // command. Decoupled from WP's command-palette mount timing
139 - // (which fires from a core-registered hook we can't reorder).
140 - $menu_map = desktop_mode_build_command_menu_map();
189 + // / `$submenu`, then injected as a `before` inline on our own
190 + // bundle — that runs synchronously before `desktop.min.js`
191 + // boots the shell harvester, so the lookup is guaranteed
192 + // populated by the time `src/commands/shell-harvester.ts`
193 + // classifies any command. Decoupled from WP's command-palette
194 + // mount timing (which fires from a core-registered hook we
195 + // can't reorder) — and, since the palette bundles went lazy,
196 + // from whether they have loaded at all.
197 + $menu_map = openstation_build_command_menu_map();
141 198 wp_add_inline_script(
142 - 'desktop-mode',
143 - 'window.__desktopModeMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
199 + 'openstation',
200 + 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
144 201 'before'
145 202 );
146 203 }
147 204
148 205 // Pass configuration to JavaScript.
149 - global $title, $pagenow, $parent_file, $menu;
206 + global $title, $parent_file, $menu;
150 207
151 208 $menu_icon = 'dashicons-admin-generic';
152 209 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
153 210 foreach ( $menu as $item ) {
@@ -159,72 +216,125 @@
159 216 }
160 217
161 218 // Build dock items from the admin menu. Core pages are ordered
162 219 // first (Dashboard, Posts, Plugins, Users, Settings, …), then
163 - // plugin-contributed top-level routes. `desktop_mode_dock_placement`
220 + // plugin-contributed top-level routes. `openstation_dock_placement`
164 221 // is the per-item filter escape hatch for hiding. Shared with the
165 222 // REST menu endpoint so live refreshes (post plugin-activation)
166 223 // produce the same ordering as the boot payload.
167 - $menu_payload = desktop_mode_build_menu_payload();
168 - $dock_items = $menu_payload['dockItems'];
169 - $native_windows = isset( $menu_payload['nativeWindows'] )
224 + $menu_payload = openstation_build_menu_payload();
225 + $dock_items = $menu_payload['dockItems'];
226 + $native_windows = isset( $menu_payload['nativeWindows'] )
170 227 ? $menu_payload['nativeWindows']
171 228 : array();
172 - $server_widgets = isset( $menu_payload['serverWidgets'] )
229 +
230 + // The BOOT page prints every registry window's template as a real
231 + // `<template>` tag (`openstation_render_native_window_templates()`,
232 + // admin_footer @ 20 — before footer scripts, so the tags are in
233 + // the DOM before the shell boots and `ensureTemplate()` adopts
234 + // them by id). The payload's `templateHtml` copy exists for the
235 + // MID-SESSION path — a bridge or probe payload delivering a
236 + // window whose plugin activated after the page rendered — so on
237 + // the boot config it is ~27 KB of the same markup twice. Strip it
238 + // here, and only here: the bridge and probe payloads keep theirs.
239 + foreach ( $native_windows as &$native_window_row ) {
240 + if ( is_array( $native_window_row ) ) {
241 + $native_window_row['templateHtml'] = '';
242 + }
243 + }
244 + unset( $native_window_row );
245 + $native_window_script_data = isset( $menu_payload['nativeWindowScriptData'] )
246 + ? $menu_payload['nativeWindowScriptData']
247 + : array();
248 + $server_widgets = isset( $menu_payload['serverWidgets'] )
173 249 ? $menu_payload['serverWidgets']
174 250 : array();
175 - $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
251 + $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
176 252 ? $menu_payload['serverWallpapers']
177 253 : array();
178 - $server_command_scripts = isset( $menu_payload['serverCommandScripts'] )
254 + $server_command_scripts = isset( $menu_payload['serverCommandScripts'] )
179 255 ? $menu_payload['serverCommandScripts']
180 256 : array();
181 - $server_commands = isset( $menu_payload['serverCommands'] )
257 + $server_commands = isset( $menu_payload['serverCommands'] )
182 258 ? $menu_payload['serverCommands']
183 259 : array();
184 - $server_settings_tab_scripts = isset( $menu_payload['serverSettingsTabScripts'] )
260 + $server_settings_tab_scripts = isset( $menu_payload['serverSettingsTabScripts'] )
185 261 ? $menu_payload['serverSettingsTabScripts']
186 262 : array();
187 - $server_settings_tabs = isset( $menu_payload['serverSettingsTabs'] )
263 + $server_settings_tabs = isset( $menu_payload['serverSettingsTabs'] )
188 264 ? $menu_payload['serverSettingsTabs']
189 265 : array();
190 266 $server_dock_rail_renderer_scripts = isset( $menu_payload['serverDockRailRendererScripts'] )
191 267 ? $menu_payload['serverDockRailRendererScripts']
192 268 : array();
193 - $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] )
269 + $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] )
194 270 ? $menu_payload['serverTitleBarButtonScripts']
195 271 : array();
196 - $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] )
272 + $server_window_action_scripts = isset( $menu_payload['serverWindowActionScripts'] )
273 + ? $menu_payload['serverWindowActionScripts']
274 + : array();
275 + $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] )
197 276 ? $menu_payload['serverWindowThemeScripts']
198 277 : array();
199 - $server_window_themes = isset( $menu_payload['serverWindowThemes'] )
278 + $server_window_themes = isset( $menu_payload['serverWindowThemes'] )
200 279 ? $menu_payload['serverWindowThemes']
201 280 : array();
202 - $server_window_control_scripts = isset( $menu_payload['serverWindowControlScripts'] )
281 + $server_window_control_scripts = isset( $menu_payload['serverWindowControlScripts'] )
203 282 ? $menu_payload['serverWindowControlScripts']
204 283 : array();
205 - $server_window_controls = isset( $menu_payload['serverWindowControls'] )
284 + $server_window_controls = isset( $menu_payload['serverWindowControls'] )
206 285 ? $menu_payload['serverWindowControls']
207 286 : array();
208 - $server_window_slot_scripts = isset( $menu_payload['serverWindowSlotScripts'] )
287 + $server_window_slot_scripts = isset( $menu_payload['serverWindowSlotScripts'] )
209 288 ? $menu_payload['serverWindowSlotScripts']
210 289 : array();
211 - $server_window_slots = isset( $menu_payload['serverWindowSlots'] )
290 + $server_window_slots = isset( $menu_payload['serverWindowSlots'] )
212 291 ? $menu_payload['serverWindowSlots']
213 292 : array();
214 - $server_window_chrome_scripts = isset( $menu_payload['serverWindowChromeScripts'] )
293 + $server_window_chrome_scripts = isset( $menu_payload['serverWindowChromeScripts'] )
215 294 ? $menu_payload['serverWindowChromeScripts']
216 295 : array();
217 - $server_window_chromes = isset( $menu_payload['serverWindowChromes'] )
296 + $server_window_chromes = isset( $menu_payload['serverWindowChromes'] )
218 297 ? $menu_payload['serverWindowChromes']
219 298 : array();
220 - $server_window_notices = isset( $menu_payload['serverWindowNotices'] )
299 + $server_window_notices = isset( $menu_payload['serverWindowNotices'] )
221 300 ? $menu_payload['serverWindowNotices']
222 301 : array();
223 - $server_games = isset( $menu_payload['serverGames'] )
302 + $server_games = isset( $menu_payload['serverGames'] )
224 303 ? $menu_payload['serverGames']
225 304 : array();
226 - $desktop_icons = isset( $menu_payload['desktopIcons'] )
305 + // Boot-time copy of the desktop-theme library. Without it the
306 + // shell's registry seeds EMPTY, and the consequences are subtle
307 + // rather than obvious: PHP has already applied the user's theme
308 + // server-side (stylesheet + shell attribute), but the client
309 + // can't resolve the slug to an entry, so it believes nothing is
310 + // active. Themed ICONS never paint, and switching back to the
311 + // system default no-ops the first time — `applyDesktopTheme()`
312 + // dedupes on an `activeId` that was never set.
313 + $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
314 + ? $menu_payload['serverDesktopThemes']
315 + : array();
316 +
317 + // Slim the theme library for BOOT: `cssText` and `tokens` are
318 + // each ~20 KB per theme, and neither is read at boot — the ACTIVE
319 + // theme's stylesheet is server-delivered (see
320 + // `openstation_enqueue_desktop_theme_style()`, whose stamp
321 + // `bootAlreadyApplied()` detects), and an inactive theme's CSS
322 + // only matters at the moment the user picks it in the Preferences
323 + // picker — which fetches the full entries from
324 + // `GET desktop-mode/v1/desktop-themes` (`ensureFullDesktopThemes()`
325 + // client-side). `cssDeferred` marks the gap so the shell can tell
326 + // a slimmed entry from a theme that genuinely ships no CSS.
327 + // Bridge and probe payloads keep full entries.
328 + foreach ( $server_desktop_themes as &$desktop_theme_row ) {
329 + if ( is_array( $desktop_theme_row ) ) {
330 + $desktop_theme_row['cssText'] = '';
331 + $desktop_theme_row['tokens'] = new stdClass();
332 + $desktop_theme_row['cssDeferred'] = true;
333 + }
334 + }
335 + unset( $desktop_theme_row );
336 + $desktop_icons = isset( $menu_payload['desktopIcons'] )
227 337 ? $menu_payload['desktopIcons']
228 338 : array();
229 339
230 340 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -229,25 +339,33 @@
229 339
230 340 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
231 341 // file types and openers ship as metadata only; the JS side
232 342 // holds the executable handlers and resolves on double-click.
233 - $server_file_types = function_exists( 'desktop_mode_build_file_types_payload' )
234 - ? desktop_mode_build_file_types_payload()
343 + $server_file_types = function_exists( 'openstation_build_file_types_payload' )
344 + ? openstation_build_file_types_payload()
235 345 : array();
236 - $server_file_openers = function_exists( 'desktop_mode_build_file_openers_payload' )
237 - ? desktop_mode_build_file_openers_payload()
346 + $server_file_openers = function_exists( 'openstation_build_file_openers_payload' )
347 + ? openstation_build_file_openers_payload()
238 348 : array();
239 - $user_file_associations = function_exists( 'desktop_mode_get_user_file_associations' )
240 - ? desktop_mode_get_user_file_associations( get_current_user_id() )
349 + // Entries in the menu payload carry dependency handles; their
350 + // payloads ride once in `scriptDepPayloads` (GH#892). The file
351 + // lists stay whole: no sync module reads them on the client, and
352 + // compacting them here was the only write to the map after the
353 + // menu payload froze its own, so a refresh could not match it.
354 + $script_dep_payloads = isset( $menu_payload['scriptDepPayloads'] )
355 + ? (array) $menu_payload['scriptDepPayloads']
241 356 : array();
242 - $server_wallpaper_menu_items = function_exists( 'desktop_mode_build_wallpaper_menu_items' )
243 - ? desktop_mode_build_wallpaper_menu_items()
357 + $user_file_associations = function_exists( 'openstation_get_user_file_associations' )
358 + ? openstation_get_user_file_associations( get_current_user_id() )
244 359 : array();
360 + $server_wallpaper_menu_items = function_exists( 'openstation_build_wallpaper_menu_items' )
361 + ? openstation_build_wallpaper_menu_items()
362 + : array();
245 363
246 364 /*
247 365 * OS-file drop config — what the browser drop manager will
248 366 * accept when the user drags a file from their native desktop
249 - * onto any surface inside Desktop Mode (wallpaper, a folder,
367 + * onto any surface inside OpenStation (wallpaper, a folder,
250 368 * a window, or a chromeless iframe). The allowed-mimes list is
251 369 * the user-scoped `get_allowed_mime_types()` (already capability
252 370 * gated by WordPress); the size cap is `wp_max_upload_size()`.
253 371 *
@@ -260,14 +378,12 @@
260 378 : array();
261 379 /**
262 380 * Filter the allowed-mime map used by the OS-file drop manager.
263 381 *
264 - * @since 0.30.0
265 - *
266 382 * @param array<string,string> $mimes_map `ext => mime-type` map (same shape `get_allowed_mime_types()` returns).
267 383 * @param int $user_id The current user id.
268 384 */
269 - $drop_allowed_mimes_map = apply_filters( 'desktop_mode_drop_allowed_mimes', $drop_allowed_mimes_map, get_current_user_id() );
385 + $drop_allowed_mimes_map = apply_filters( 'openstation_drop_allowed_mimes', $drop_allowed_mimes_map, get_current_user_id() );
270 386 $drop_allowed_mimes_map = is_array( $drop_allowed_mimes_map ) ? $drop_allowed_mimes_map : array();
271 387 $drop_allowed_mimes = array_values( array_unique( array_values( $drop_allowed_mimes_map ) ) );
272 388
273 389 $drop_max_size = (int) wp_max_upload_size();
@@ -275,14 +391,12 @@
275 391 * Filter the per-file size cap (in bytes) used by the OS-file
276 392 * drop manager. Returning `0` disables the client-side cap —
277 393 * the server still enforces its own.
278 394 *
279 - * @since 0.30.0
280 - *
281 395 * @param int $max_size Default `wp_max_upload_size()`.
282 396 * @param int $user_id The current user id.
283 397 */
284 - $drop_max_size = (int) apply_filters( 'desktop_mode_drop_max_size', $drop_max_size, get_current_user_id() );
398 + $drop_max_size = (int) apply_filters( 'openstation_drop_max_size', $drop_max_size, get_current_user_id() );
285 399
286 400 /**
287 401 * Filter the master OS-file drop enable gate. Lets plugins
288 402 * disable the drop manager by role / capability beyond the
@@ -288,15 +402,13 @@
288 402 * disable the drop manager by role / capability beyond the
289 403 * default `upload_files` check (e.g. only for admins, or
290 404 * only on specific multisite blogs).
291 405 *
292 - * @since 0.30.0
293 - *
294 406 * @param bool $enabled Default — `current_user_can( 'upload_files' )`.
295 407 * @param int $user_id The current user id.
296 408 */
297 409 $drop_enabled = (bool) apply_filters(
298 - 'desktop_mode_drop_enabled',
410 + 'openstation_drop_enabled',
299 411 current_user_can( 'upload_files' ),
300 412 get_current_user_id()
301 413 );
302 414
@@ -307,48 +419,61 @@
307 419 'maxSize' => $drop_max_size,
308 420 );
309 421
310 422 // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant,
311 - // About-scene, OS Settings panel, shell-overlays, window-system)
423 + // OS Settings panel, shell-overlays, window-system)
312 424 // is `<script>`-injected by the main bundle on demand — they don't
313 425 // go through `wp_register_script`, so they don't pick up WordPress's
314 426 // usual `?ver=<filemtime>` cache-buster. Without one, the browser
315 427 // happily serves a stale cached copy across plugin updates that
316 - // don't bump `DESKTOP_MODE_VERSION`, and the main bundle's loader
428 + // don't bump `OPENSTATION_VERSION`, and the main bundle's loader
317 429 // fires a `<script>`-loaded event for a file that's missing the
318 - // fresh `window.desktopMode*` factory the new code expects.
430 + // fresh `window.openStation*` factory the new code expects.
319 431 //
320 432 // Mirror the `$built_version( … )` helper in `includes/assets.php`:
321 433 // prefer the on-disk mtime of the actual file, fall back to the
322 434 // plugin version when the file is missing (dev environments where
323 435 // the bundle hasn't been built yet).
324 - $suffix = desktop_mode_asset_suffix();
436 + $suffix = openstation_asset_suffix();
325 437 $lazy_bundle_url = static function ( $base ) use ( $suffix ) {
326 - $path = DESKTOP_MODE_DIR . 'assets/js/' . $base . $suffix . '.js';
438 + $path = OPENSTATION_DIR . 'assets/js/' . $base . $suffix . '.js';
327 439 $ver = file_exists( $path )
328 440 ? (string) filemtime( $path )
329 - : DESKTOP_MODE_VERSION;
441 + : OPENSTATION_VERSION;
330 442 return esc_url_raw(
331 - DESKTOP_MODE_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
443 + OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
332 444 );
333 445 };
334 446
335 - // Build the current page URL from $pagenow + $_GET. Strip the portal
336 - // markers so the derived window ID matches what the dock would produce
337 - // for the same page — otherwise auto-opening the entry window and
338 - // clicking the same dock icon would create a duplicate.
339 - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
340 - unset( $current_query[ DESKTOP_MODE_PORTAL_FLAG ], $current_query[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] );
341 - $current_page = admin_url( $pagenow ) . ( ! empty( $current_query ) ? '?' . http_build_query( $current_query ) : '' );
447 + // The page the shell opens first. On the shell screen it is the
448 + // validated `target` query arg (else the session's focused window,
449 + // the default window, the Dashboard); on a solo boot it is the
450 + // request's own URL. Either way the frozen portal flags are gone
451 + // from it, so the derived window id matches what the dock would
452 + // produce for the same page — otherwise auto-opening the entry
453 + // window and clicking the same dock icon would create a duplicate.
454 + $boot_target = openstation_shell_boot_target();
455 + $current_page = $boot_target['url'];
456 + $from_portal = $boot_target['fromPortal'];
457 + $from_portal_intent = $boot_target['fromPortalIntent'];
342 458
343 - $from_portal = ! empty( $_GET[ DESKTOP_MODE_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
344 - $from_portal_intent = ! empty( $_GET[ DESKTOP_MODE_PORTAL_INTENT_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
459 + // On the shell screen `$title` and `$parent_file` describe the
460 + // screen ("OpenStation", no menu), not the page about to open. The
461 + // dock entry for that page is the identity the entry window folds
462 + // into, so its title and icon are the right first paint; the iframe
463 + // reports its own title once it lands either way.
464 + $current_title = wp_strip_all_tags( (string) $title );
465 + if ( openstation_is_shell_screen_request() ) {
466 + $boot_meta = openstation_shell_boot_target_meta( $current_page, $dock_items );
467 + $current_title = wp_strip_all_tags( $boot_meta['title'] );
468 + if ( '' !== $boot_meta['icon'] ) {
469 + $menu_icon = $boot_meta['icon'];
470 + }
471 + }
345 472
346 473 /**
347 474 * Filters the desktop shell configuration passed to JavaScript.
348 475 *
349 - * @since 0.1.0
350 - *
351 476 * @param array $config {
352 477 * Desktop shell configuration.
353 478 *
354 479 * @type string $currentPage The current admin page URL.
@@ -355,20 +480,20 @@
355 480 * @type string $currentTitle The current page title.
356 481 * @type string $currentIcon Dashicon class for the current page.
357 482 * @type string $adminUrl The base admin URL.
358 483 * @type string $colorScheme The active admin color scheme.
359 - * @type array $dockItems Dock items derived from the admin menu. Core WordPress pages (Dashboard, Posts, Plugins, Users, Settings, CPTs…) are ordered first; plugin-contributed top-level routes (admin.php?page=*) follow. Items hidden via `desktop_mode_dock_placement` are omitted.
360 - * @type array $nativeWindows Server-declared native windows (via `desktop_mode_register_window`). Shell registers + syncs tiles based on this list — activation/deactivation is a diff without shell reload.
361 - * @type array $serverWidgets Server-declared right-column widgets (via `desktop_mode_register_widget`). Shell syncs the widget registry + dynamically loads plugin scripts so widgets appear in the picker without a shell reload.
362 - * @type array $serverWallpapers Server-declared wallpapers (via `desktop_mode_register_wallpaper`). Same lifecycle — shell loads the plugin's JS, reads the full `WallpaperDef` from `window.desktopModeWallpapers[id]`, and registers / unregisters as plugins activate / deactivate.
363 - * @type array $serverCommandScripts Script handles opted-in via `desktop_mode_register_command_script`. Shell injects each URL on activation so commands registered by `wp.desktop.registerCommand` appear in the palette live. Deactivation unregisters any commands whose `owner` matches the departing handle.
364 - * @type array $serverCommands Server-declared command metadata (via `desktop_mode_register_command`). Advisory today — reserved for future pre-registration shims.
365 - * @type array $serverSettingsTabScripts Script handles opted-in via `desktop_mode_register_settings_tab_script`. Shell injects each URL on activation so tabs registered by `wp.desktop.registerSettingsTab` appear in the OS Settings window live. Deactivation unregisters tabs attributable to the departing handle.
366 - * @type array $serverSettingsTabs Server-declared settings-tab metadata (via `desktop_mode_register_settings_tab`). Enables live unregistration on plugin deactivation without requiring JS to set `owner`.
367 - * @type array $desktopIcons Server-declared desktop icons (via `desktop_mode_register_icon`). Rendered on the wallpaper as clickable shortcut tiles.
368 - * @type array $accentColors Swatch list for the OS Settings accent picker. Filterable via `desktop_mode_accent_colors`.
369 - * @type array $toastTypes Toast-notification type map. Filterable via `desktop_mode_toast_types`.
370 - * @type string $defaultWallpaper Wallpaper slug applied on first boot. Filterable via `desktop_mode_default_wallpaper`.
484 + * @type array $dockItems Dock items derived from the admin menu. Core WordPress pages (Dashboard, Posts, Plugins, Users, Settings, CPTs…) are ordered first; plugin-contributed top-level routes (admin.php?page=*) follow. Items hidden via `openstation_dock_placement` are omitted.
485 + * @type array $nativeWindows Server-declared native windows (via `openstation_register_window`). Shell registers + syncs tiles based on this list — activation/deactivation is a diff without shell reload.
486 + * @type array $serverWidgets Server-declared right-column widgets (via `openstation_register_widget`). Shell syncs the widget registry + dynamically loads plugin scripts so widgets appear in the picker without a shell reload.
487 + * @type array $serverWallpapers Server-declared wallpapers (via `openstation_register_wallpaper`). Same lifecycle — shell loads the plugin's JS, reads the full `WallpaperDef` from `window.openStationWallpapers[id]`, and registers / unregisters as plugins activate / deactivate.
488 + * @type array $serverCommandScripts Script handles opted-in via `openstation_register_command_script`. Shell injects each URL on activation so commands registered by `wp.os.registerCommand` appear in the palette live. Deactivation unregisters any commands whose `owner` matches the departing handle.
489 + * @type array $serverCommands Server-declared command metadata (via `openstation_register_command`). Advisory today — reserved for future pre-registration shims.
490 + * @type array $serverSettingsTabScripts Script handles opted-in via `openstation_register_settings_tab_script`. Shell injects each URL on activation so tabs registered by `wp.os.registerSettingsTab` appear in the OS Settings window live. Deactivation unregisters tabs attributable to the departing handle.
491 + * @type array $serverSettingsTabs Server-declared settings-tab metadata (via `openstation_register_settings_tab`). Enables live unregistration on plugin deactivation without requiring JS to set `owner`.
492 + * @type array $desktopIcons Server-declared desktop icons (via `openstation_register_icon`). Rendered on the wallpaper as clickable shortcut tiles.
493 + * @type array $accentColors Swatch list for the OS Settings accent picker. Filterable via `openstation_accent_colors`.
494 + * @type array $toastTypes Toast-notification type map. Filterable via `openstation_toast_types`.
495 + * @type string $defaultWallpaper Wallpaper slug applied on first boot. Filterable via `openstation_default_wallpaper`.
371 496 * @type array $session Saved session (windows, focused, updated).
372 497 * @type string $sessionUrl REST endpoint for saving the session.
373 498 * @type string $mediaUrl REST endpoint for media uploads (wp/v2/media).
374 499 * @type string $restUrl REST API root from rest_url(), safe for pretty and plain permalink installs.
@@ -376,186 +501,312 @@
376 501 * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference.
377 502 * @type bool $canUpload Whether the user holds the `upload_files` capability.
378 503 * @type string $pluginUrl Plugin base URL (no trailing slash). Used by the shell to locate vendor assets and by plugins to build asset URLs.
379 504 * @type string $pluginVersion Plugin semver string. Surfaced in the OS Settings → About tab; plugins can read it to gate features by version.
505 + * @type string $aboutFeedUrl Authenticated admin-AJAX URL that returns the cached OpenStation journal feed for the About tab.
380 506 * @type string $restNonce Nonce for the session REST endpoint.
381 - * @type string $portalUrl Canonical `/desktop-mode/` URL.
507 + * @type string $soloWindow Window id when the shell was asked to paint exactly one window (`?openstation_solo=<id>`); '' otherwise. No dock, taskbar, wallpaper or desk, and no session restore.
508 + * @type string $portalUrl Canonical `/openstation/` URL.
382 509 * @type bool $fromPortal Whether the shell was reached via the portal.
383 - * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/desktop-mode/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for.
384 - * @type array $seenIntros Slugs of one-time intro dialogs the user has dismissed (e.g. `['posts']`). Native windows gate their first-open intro on this list.
510 + * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/openstation/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for.
511 + * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`).
385 512 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
386 - * @type array $stickyNotes { available: bool } — whether the Gutenberg Guidelines experiment (the `wp_guideline` CPT + `wp_guideline_type` taxonomy) is registered. The shell skips booting the sticky-notes layer when false, avoiding the REST probes that 404 without the experiment. See `desktop_mode_sticky_notes_is_available()`.
513 + * @type bool $shellTour Whether this site offers the first-boot shell tour (`openstation_show_shell_tour`). Whether this user already had it is `seenIntros` containing `shell-tour`.
514 + * @type string $shellTourBundleUrl URL of the lazy shell-tour bundle, injected on first use.
515 + * @type array $firstRun `{ installedAt, firstEnabledAt, enabledAt }`, epoch seconds, 0 when unknown — the first-run stamps, read-only.
516 + * @type bool $rebrandNotice Whether to offer this user the one-off announcement explaining the rename from Desktop Mode to OpenStation. True only when migration 5 flagged this user as a Desktop Mode user from before the rename AND they haven't dismissed the `openstation-rebrand` intro. Only ever present in the shell config, so the announcement never reaches the classic admin.
517 + * @type array|null $usageFeedback What the one-time usage feedback prompt needs (`restUrl`), or `null` when this user is not owed it: the feature is off, they have had OpenStation on for fewer than seven days by the `openstation_enabled_at` stamp, or they already answered or dismissed the `usage-feedback` intro. Carries no user data.
518 + * @type string $usageFeedbackBundleUrl URL of the lazy `usage-feedback` bundle, the form the prompt opens.
387 519 * }
388 520 */
389 521 $config = apply_filters(
390 - 'desktop_mode_shell_config',
522 + 'openstation_shell_config',
391 523 array(
392 - 'currentPage' => esc_url( $current_page ),
393 - 'currentTitle' => wp_strip_all_tags( $title ),
394 - 'currentIcon' => sanitize_html_class( $menu_icon ),
395 - 'adminUrl' => esc_url( admin_url() ),
396 - 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ),
397 - 'dockItems' => $dock_items,
524 + 'currentPage' => esc_url( $current_page ),
525 + 'currentTitle' => $current_title,
526 + 'currentIcon' => sanitize_html_class( $menu_icon ),
527 + // `self_admin_url()`: the base a window id is derived from,
528 + // and the URL the shell leaves for on exit. Both want the
529 + // admin the screen is in, which is the network one when the
530 + // network shell screen is what rendered.
531 + 'adminUrl' => esc_url( self_admin_url() ),
532 + 'homeUrl' => esc_url( home_url( '/' ) ),
533 + // Decoded: the shell assigns this to `window.location`,
534 + // where `&amp;` would make `_wpnonce` arrive as
535 + // `amp;_wpnonce` and fail the nonce check.
536 + 'logoutUrl' => esc_url_raw(
537 + html_entity_decode( wp_logout_url(), ENT_QUOTES, 'UTF-8' )
538 + ),
539 + 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ),
540 + 'dockItems' => $dock_items,
398 541 // Baseline menu fingerprint. The shell seeds its last-known
399 542 // signature from this so the first off-allowlist menu change
400 543 // (vs. this boot state) is caught without a wasted probe. GH#325.
401 - 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
402 - 'nativeWindows' => $native_windows,
403 - 'serverWidgets' => $server_widgets,
404 - 'serverWallpapers' => $server_wallpapers,
405 - 'serverCommandScripts' => $server_command_scripts,
406 - 'serverCommands' => $server_commands,
407 - 'serverSettingsTabScripts' => $server_settings_tab_scripts,
408 - 'serverSettingsTabs' => $server_settings_tabs,
544 + 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
545 + 'nativeWindows' => $native_windows,
546 + // Handle-keyed script data the entries above reference —
547 + // one copy per bundle, not one per window. See
548 + // `openstation_collect_native_windows_payload()`.
549 + 'nativeWindowScriptData' => $native_window_script_data,
550 + 'serverWidgets' => $server_widgets,
551 + 'serverWallpapers' => $server_wallpapers,
552 + 'serverCommandScripts' => $server_command_scripts,
553 + 'serverCommands' => $server_commands,
554 + 'serverSettingsTabScripts' => $server_settings_tab_scripts,
555 + 'serverSettingsTabs' => $server_settings_tabs,
409 556 'serverDockRailRendererScripts' => $server_dock_rail_renderer_scripts,
410 - 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts,
411 - 'serverWindowThemeScripts' => $server_window_theme_scripts,
412 - 'serverWindowThemes' => $server_window_themes,
413 - 'serverWindowControlScripts' => $server_window_control_scripts,
414 - 'serverWindowControls' => $server_window_controls,
415 - 'serverWindowSlotScripts' => $server_window_slot_scripts,
416 - 'serverWindowSlots' => $server_window_slots,
417 - 'serverWindowChromeScripts' => $server_window_chrome_scripts,
418 - 'serverWindowChromes' => $server_window_chromes,
419 - 'serverWindowNotices' => $server_window_notices,
557 + 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts,
558 + 'serverWindowActionScripts' => $server_window_action_scripts,
559 + 'serverWindowThemeScripts' => $server_window_theme_scripts,
560 + 'serverWindowThemes' => $server_window_themes,
561 + 'serverWindowControlScripts' => $server_window_control_scripts,
562 + 'serverWindowControls' => $server_window_controls,
563 + 'serverWindowSlotScripts' => $server_window_slot_scripts,
564 + 'serverWindowSlots' => $server_window_slots,
565 + 'serverWindowChromeScripts' => $server_window_chrome_scripts,
566 + 'serverWindowChromes' => $server_window_chromes,
567 + 'serverWindowNotices' => $server_window_notices,
420 568 // Boot-time copy of the payload's `serverGames` — the same
421 569 // list the live-refresh path applies. Without it the games
422 570 // registry only fills after the first chromeless
423 571 // full-payload refresh and the Games hub boots empty.
424 - 'serverGames' => $server_games,
425 - 'desktopIcons' => $desktop_icons,
426 - 'serverFileTypes' => $server_file_types,
427 - 'serverFileOpeners' => $server_file_openers,
428 - 'userFileAssociations' => $user_file_associations,
429 - 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
572 + 'serverGames' => $server_games,
573 + 'serverDesktopThemes' => $server_desktop_themes,
574 + 'desktopIcons' => $desktop_icons,
575 + 'serverFileTypes' => $server_file_types,
576 + 'serverFileOpeners' => $server_file_openers,
577 + // Handle => dependency payload for every `scriptDeps` list in
578 + // this config; see `openstation_compact_script_deps()`.
579 + 'scriptDepPayloads' => (object) $script_dep_payloads,
580 + 'userFileAssociations' => $user_file_associations,
581 + 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
430 582 // Pinned-notes REST base (`includes/notes/rest.php`). The
431 583 // notes layer boots only when this is present.
432 - 'notesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/notes' ) ),
584 + 'notesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/notes' ) ),
433 585 // Gates the "Convert to post" note affordance — the convert
434 586 // route (and its dock drop target) only make sense for users
435 587 // who can author posts.
436 - 'canCreatePosts' => current_user_can( 'edit_posts' ),
437 - 'serverWallpaperMenuItems' => $server_wallpaper_menu_items,
438 - 'accentColors' => desktop_mode_get_accent_colors(),
439 - 'toastTypes' => desktop_mode_get_toast_types(),
440 - 'coreUpdate' => desktop_mode_get_core_update(),
441 - 'coreNotices' => desktop_mode_get_core_notices(),
442 - 'pluginNotices' => desktop_mode_get_plugin_notices(),
443 - 'defaultWallpaper' => desktop_mode_get_default_wallpaper(),
444 - 'session' => desktop_mode_get_session( get_current_user_id() ),
445 - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
446 - 'restUrl' => esc_url_raw( rest_url() ),
447 - 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
448 - 'dropConfig' => $drop_config,
449 - 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
450 - 'defaultWindow' => desktop_mode_get_default_window( get_current_user_id() ),
451 - 'canUpload' => current_user_can( 'upload_files' ),
452 - 'pluginUrl' => esc_url_raw( untrailingslashit( DESKTOP_MODE_URL ) ),
453 - 'pluginVersion' => DESKTOP_MODE_VERSION,
454 - 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
588 + 'canCreatePosts' => current_user_can( 'edit_posts' ),
589 + 'serverWallpaperMenuItems' => $server_wallpaper_menu_items,
590 + 'accentColors' => openstation_get_accent_colors(),
591 + 'toastTypes' => openstation_get_toast_types(),
592 + 'coreUpdate' => openstation_get_core_update(),
593 + 'coreNotices' => openstation_get_core_notices(),
594 + 'pluginNotices' => openstation_get_plugin_notices(),
595 + 'defaultWallpaper' => openstation_get_default_wallpaper(),
596 + 'session' => openstation_get_session( get_current_user_id() ),
597 + // The session route runs in the main site's blog context
598 + // whichever desktop posts to it, so the network screen's
599 + // URL says which session it is addressing — see
600 + // `openstation_rest_session_network()`.
601 + 'sessionUrl' => esc_url_raw(
602 + is_network_admin()
603 + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) )
604 + : rest_url( 'desktop-mode/v1/session' )
605 + ),
606 + 'restUrl' => esc_url_raw( rest_url() ),
607 + 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
608 + 'dropConfig' => $drop_config,
609 + 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
610 + 'defaultWindow' => openstation_get_default_window( get_current_user_id() ),
611 + 'canUpload' => current_user_can( 'upload_files' ),
612 + 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ),
613 + 'pluginVersion' => OPENSTATION_VERSION,
614 + 'aboutFeedUrl' => esc_url_raw(
615 + add_query_arg(
616 + array(
617 + 'action' => 'openstation_about_feed',
618 + 'nonce' => wp_create_nonce( 'openstation_about_feed' ),
619 + ),
620 + admin_url( 'admin-ajax.php' )
621 + )
622 + ),
623 + 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
455 624 // URL of the AI Assistant lazy bundle. The main bundle
456 - // ships a stub matching the public `wp.desktop.ai` API; the
625 + // ships a stub matching the public `wp.os.ai` API; the
457 626 // stub `<script>`-injects this URL the first time the user
458 627 // opens the assistant. Picking `.js` vs `.min.js` here keeps
459 628 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
460 - 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
461 - // URL of the About-scene lazy bundle. The OS Settings →
462 - // About tab loads this on first mount; ~25 kB PixiJS
463 - // particle scene that would otherwise ship in the main
464 - // bundle for every shell load.
465 - 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ),
466 - // URL of the OS Settings panel lazy bundle. Injected by
467 - // the main bundle's `OsSettings.renderPanel()` stub on
468 - // the user's first Settings open. Holds every section
469 - // renderer + the `<wpd-*>` components only the panel
470 - // uses, so nothing about Settings ships in
471 - // `desktop.min.js` for users who never open it.
472 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
629 + 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
473 630 // URL of the shell-overlays lazy bundle. Pre-loaded by
474 631 // the main bundle after first paint so action-triggered
475 632 // overlays (toast, confirm dialog, context menus) feel
476 633 // instant the first time they fire.
477 - 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
634 + 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
635 + // The shell-bundle diet: features whose right moment is a
636 + // user gesture (or a presence signal) ride their own
637 + // bundles instead of the boot-critical `desktop[.min].js`.
638 + // Each sentinel in the shell loads its bundle at that
639 + // moment — see the entry file each bundle names.
640 + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ),
641 + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ),
642 + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ),
643 + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ),
644 + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ),
645 + // Presence hint for the notes sentinel: a desktop with no
646 + // notes skips the notes bundle AND the boot-time list
647 + // request. Two id-only existence probes at most.
648 + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' )
649 + ? openstation_notes_user_has_any()
650 + : false,
651 + // URL of the full `<os-*>` component kit. The shell
652 + // never loads this — its own bundles import the
653 + // components they render. It exists for
654 + // `wp.os.loadComponents()`, i.e. for plugin code that
655 + // CANNOT import: a plugin shipped as a zip has no path
656 + // to this repo at build time, so before this URL its
657 + // only routes to a `<os-switch>` were to bundle a second
658 + // copy or hand-roll one. Shipping the URL costs one
659 + // string and keeps the SCRIPT_DEBUG choice server-side.
660 + 'componentsBundleUrl' => $lazy_bundle_url( 'os-components' ),
661 + // Mio — the desk companion. `mio` carries the
662 + // appearance + physics (see `openstation_mio_config()`);
663 + // `mioBundleUrl` is the lazy PixiJS bundle the shell
664 + // controller injects the first time a user switches the
665 + // Mio on from its dock tile. Shipping the URL
666 + // unconditionally costs one short string and keeps the
667 + // SCRIPT_DEBUG choice server-side, matching every other
668 + // lazy bundle here.
669 + //
670 + // Both keys ship whether or not the user has Mio on,
671 + // and that is the whole of its cost to a shell that doesn't:
672 + // ~470 bytes gzipped of config, plus a URL. No script, no
673 + // style, no PixiJS. The config has to be here rather than
674 + // fetched on first toggle, or the `openstation_mio_config`
675 + // filter would silently not apply until the next reload.
676 + 'mio' => openstation_mio_config(),
677 + 'mioBundleUrl' => $lazy_bundle_url( 'mio' ),
478 678 // URL of the lazy window-system bundle (Stage 11).
479 679 // Holds the `Window` class and its DOM / pointer / tab /
480 - // chrome helpers — the single largest module in the pre-
481 - // 0.8.4 main bundle. Loaded on first `windowManager.open()`
482 - // / `openNew()` call (both async since 0.8.4); pre-loaded
680 + // chrome helpers — the single largest module split out of
681 + // the main bundle. Loaded on first `windowManager.open()`
682 + // / `openNew()` call (both async); pre-loaded
483 683 // by the shell after first paint when no session is being
484 684 // restored and no `openCurrentPage` will fire.
485 - 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
685 + 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
686 + // URL of the lazy phone-layer bundle. Injected by the main
687 + // bundle only when the mode resolves to `mobile`, so a
688 + // desktop never fetches it. `mode` carries the preference
689 + // and breakpoints the first-paint head stamp already used,
690 + // plus the server's default tab-bar pins — see
691 + // `includes/mobile.php`.
692 + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ),
693 + 'mode' => openstation_mode_config( get_current_user_id() ),
486 694 // URL of the item-visibility-menu lazy bundle — the
487 695 // right-click "hide from dock / desktop" menu. Injected by
488 696 // the main bundle's loader shim on the first right-click.
489 - 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
697 + 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
698 + // URL of the workspace-wizard lazy bundle — the modal
699 + // behind "Edit this workspace…". Injected by the main
700 + // bundle's loader shim on first open.
701 + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ),
702 + // Server-side view of the workspace templates, so a plugin
703 + // can add or drop one from PHP. The client merges these
704 + // with its own built-ins by id — see
705 + // `src/workspaces/server-sync.ts`.
706 + 'workspacePresets' => openstation_workspace_presets(),
490 707 // URL of the release-card lazy bundle — the vinyl core-
491 708 // update announcement. Injected by `maybeShowUpdate()` only
492 709 // when a core update is actually pending.
493 - 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
494 - 'restNonce' => wp_create_nonce( 'wp_rest' ),
495 - 'osSettings' => desktop_mode_get_os_settings( get_current_user_id() ),
496 - 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ),
497 - 'seenIntros' => desktop_mode_get_seen_intros( get_current_user_id() ),
498 - 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ),
499 - // Sticky notes ride on Gutenberg's Guidelines experiment
500 - // (the `wp_guideline` CPT + `wp_guideline_type` taxonomy).
501 - // When that experiment isn't active the `wp/v2/guidelines`
502 - // + `wp/v2/wp_guideline_type` probes 404 — harmless but
503 - // noisy — so the shell skips booting the layer entirely.
504 - 'stickyNotes' => array(
505 - 'available' => desktop_mode_sticky_notes_is_available(),
506 - ),
507 - 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
508 - 'aiSearchStreamUrl' => esc_url_raw( add_query_arg( 'action', 'desktop_mode_ai_search_stream', admin_url( 'admin-ajax.php' ) ) ),
710 + 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
711 + 'restNonce' => wp_create_nonce( 'wp_rest' ),
712 + // Non-empty when the shell was asked to paint exactly one
713 + // window and nothing else. See `OPENSTATION_SOLO_FLAG`.
714 + 'soloWindow' => openstation_solo_window_id(),
715 + 'osSettings' => openstation_get_os_settings( get_current_user_id() ),
716 + 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ),
717 + 'seenIntros' => openstation_get_seen_intros( get_current_user_id() ),
718 + 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ),
719 + // True only for a user migration 5 flagged as a Desktop Mode
720 + // user from before the rename, who hasn't dismissed the
721 + // announcement yet. Same value that gated `os-announce`
722 + // above; the dialog cannot paint without that stylesheet, so
723 + // the two must not diverge.
724 + 'rebrandNotice' => $show_rebrand_notice,
725 + // The first-boot shell tour: whether this site offers it
726 + // (the `openstation_show_shell_tour` filter), and the lazy
727 + // bundle that runs it. Whether THIS user already had it is
728 + // `seenIntros` containing `shell-tour`; the shell reads that
729 + // itself so a reset can replay the tour without a new boot.
730 + 'shellTour' => openstation_should_offer_shell_tour( get_current_user_id() ),
731 + 'shellTourBundleUrl' => $lazy_bundle_url( 'shell-tour' ),
732 + // The first-run stamps, read-only, epoch seconds (0 when
733 + // unknown): when the plugin was installed, when anyone first
734 + // enabled it, and when this user did. See
735 + // `includes/first-run/stamps.php`.
736 + 'firstRun' => openstation_first_run_config( get_current_user_id() ),
737 + // Null for everyone but a user who has had OpenStation on
738 + // long enough and has not answered yet; the gate lives in
739 + // `includes/feedback/usage.php`. The form is a lazy bundle
740 + // fetched only when the user says yes to the prompt.
741 + 'usageFeedback' => function_exists( 'openstation_usage_feedback_config' ) ? openstation_usage_feedback_config() : null,
742 + 'usageFeedbackBundleUrl' => $lazy_bundle_url( 'usage-feedback' ),
743 + 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
509 744 // AI assistant availability + per-user toggle. Drives whether the
510 745 // Cmd+K palette and admin-bar icon appear, and the setup placeholder.
511 - 'aiAssistant' => function_exists( 'desktop_mode_ai_assistant_config' )
512 - ? desktop_mode_ai_assistant_config()
746 + 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' )
747 + ? openstation_ai_assistant_config()
513 748 : null,
514 749 // Lets the Features tab re-check provider availability without a
515 750 // reload after a connector is configured in Settings → Connectors.
516 - 'aiStatusUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/status' ) ),
517 - 'extendedOptions' => current_user_can( 'manage_options' ) ? desktop_mode_get_extended_options() : null,
518 - 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ),
751 + 'aiStatusUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/status' ) ),
752 + 'extendedOptions' => current_user_can( 'manage_options' ) ? openstation_get_extended_options() : null,
753 + 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ),
519 754 // Site-wide games kill switch (Extended options). Exposed to
520 755 // every user — the shell skips the challenges Heartbeat
521 756 // channel when the framework is off.
522 - 'gamesEnabled' => desktop_mode_games_enabled(),
523 - // Comments-window AI moderation toggle — surfaced at the
524 - // shell level so the OS Settings → Features tab can render
525 - // the toggle without depending on the Comments window
526 - // being registered for this user. URL is the same
527 - // endpoint the comments-window config exposes; state is
528 - // `null` for non-admins (the UI hides the row entirely).
529 - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
530 - // Non-null only for admins on a site where the Core AI stack is
531 - // present. Comment scoring routes through the AI Client (WP 7.0+),
532 - // so on older WordPress the whole row is hidden — same as the
533 - // assistant toggle — rather than shown disabled pointing at a
534 - // Settings → Connectors screen that doesn't exist there.
535 - 'commentsAi' => (
536 - current_user_can( 'manage_options' )
537 - && function_exists( 'desktop_mode_ai_is_available' )
538 - && desktop_mode_ai_is_available()
539 - )
540 - ? array(
541 - 'enabled' => function_exists( 'desktop_mode_comments_ai_is_enabled' )
542 - ? desktop_mode_comments_ai_is_enabled()
543 - : false,
544 - 'providerConfigured' => function_exists( 'desktop_mode_comments_ai_provider_configured' )
545 - ? desktop_mode_comments_ai_provider_configured()
546 - : false,
547 - )
548 - : null,
549 - 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
550 - 'portalUrl' => esc_url( desktop_mode_portal_url() ),
551 - 'fromPortal' => $from_portal,
552 - 'fromPortalIntent' => $from_portal_intent,
553 - 'pwa' => array(
554 - 'manifestUrl' => esc_url_raw( desktop_mode_pwa_manifest_url() ),
555 - 'swUrl' => esc_url_raw( desktop_mode_pwa_sw_url() ),
757 + 'gamesEnabled' => openstation_games_enabled(),
758 + 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
759 + // Null on single-site installs; its `networkAdmin` null
760 + // without `manage_network`, which is what keeps the dock
761 + // tile from registering.
762 + 'multisite' => openstation_multisite_payload(),
763 + 'portalUrl' => esc_url( openstation_portal_url() ),
764 + 'fromPortal' => $from_portal,
765 + 'fromPortalIntent' => $from_portal_intent,
766 + // One-shot like the boot target: a switch from another
767 + // site's overview lands in this one's.
768 + 'landInOverview' => openstation_shell_lands_in_overview(),
769 + 'arrivalDirection' => openstation_shell_arrival_direction(),
770 + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null,
771 + 'pwa' => array(
772 + 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
773 + 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ),
774 + // Extensionless retry target for hosts whose nginx 404s
775 + // virtual .js paths before WordPress runs (WordPress.com).
776 + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ),
777 + // The site's home path — the scope the registration
778 + // asks for, so a subdirectory network's sites each get
779 + // their own worker instead of fighting over the root.
780 + 'swScope' => openstation_pwa_sw_scope(),
781 + // The worker's per-user flags, computed HERE rather than
782 + // baked into the served `sw.js`.
783 + //
784 + // A service worker is origin-wide but these are per-user
785 + // preferences, so putting them in the script bytes made
786 + // the body differ between an anonymous and a logged-in
787 + // request — and any in-scope logged-out navigation then
788 + // installed a "new" worker, which at the time reloaded
789 + // the shell. The bytes are identical for everyone now;
790 + // the shell posts these to the worker at boot.
791 + //
792 + // Computed server-side, not read from the settings
793 + // snapshot client-side, because
794 + // `openstation_pwa_admin_asset_cache_enabled()` applies
795 + // the `openstation_pwa_admin_asset_cache` filter — an
796 + // operator's site-wide veto has to keep working.
797 + 'swConfig' => array(
798 + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(),
799 + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ),
800 + ),
801 + // The build this shell document belongs to. When a new
802 + // worker takes over mid-session the shell asks it for
803 + // the stamp it was served with and compares; only a
804 + // difference — the shell's own files changed on the
805 + // server — offers the user a reload. Never automatic.
806 + 'shellBuild' => openstation_shell_build_stamp(),
556 807 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
557 - 'state' => desktop_mode_pwa_get_user_state( get_current_user_id() ),
808 + 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
558 809 // Mirrors the manifest's `name` field — used by the
559 810 // install pill so the button reads "Install <site>"
560 811 // rather than "Install <current page>" (which would
561 812 // be misleading: we install the whole site as an
@@ -561,31 +812,113 @@
561 812 // be misleading: we install the whole site as an
562 813 // app, not the dashboard window the user happens to
563 814 // be viewing).
564 815 'appName' => get_bloginfo( 'name' ),
565 - // Operators set the `desktop_mode_pwa_force_replace_sw`
816 + // Operators set the `openstation_pwa_force_replace_sw`
566 817 // filter to `true` when another root-scope service
567 - // worker on the origin is blocking desktop-mode
818 + // worker on the origin is blocking openstation
568 819 // installability (foreign-SW guard in
569 820 // `src/pwa/sw-register.ts`). Default `false` preserves
570 821 // the polite behaviour where we yield to existing PWAs.
571 - 'forceReplaceSw' => desktop_mode_pwa_force_replace_sw(),
822 + 'forceReplaceSw' => openstation_pwa_force_replace_sw(),
572 823 ),
824 + // Ordered Core command-palette asset manifest, replayed on
825 + // first palette invocation. `null` on pre-6.9 sites.
826 + 'commandPalette' => $command_palette,
827 + // Stylesheets for shell surfaces that render on demand —
828 + // the Preferences panel, the AI assistant, the bug-report
829 + // window. None of them is a server-registered native
830 + // window (they are built client-side by the shell
831 + // bundle), so the `styles` companion mechanism can't
832 + // carry their CSS; instead the shell injects each sheet
833 + // the first time its surface opens, via
834 + // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
835 + // Same resolved shape a native window's `styleUrl` /
836 + // `styleInline` travels in.
837 + // Which of the `deferredStyles` entries a game needs.
838 + // `launchGame()` injects these before the window paints.
839 + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' )
840 + ? openstation_games_style_handles()
841 + : array(),
842 + 'deferredStyles' => openstation_build_deferred_styles(
843 + array_merge(
844 + array(
845 + 'desktop-mode-ai-assistant',
846 + 'desktop-mode-bug-report',
847 + // The explorer's shared sheet. It rides the WP
848 + // Explorer APP as a companion style, but the
849 + // desktop FOLDER window paints its preview pane
850 + // with the same `os-my-wordpress__*` classes and
851 + // — being a native window opened straight from
852 + // JS — carries no companion styles of its own.
853 + // Without this, the pane rendered unstyled until
854 + // the explorer had been opened once in the
855 + // session.
856 + 'desktop-mode-my-wordpress',
857 + ),
858 + // The Games sheets. They also ride the hub window as
859 + // companion styles, but a game is reachable without
860 + // the hub — the challenge toast, solo mode, and
861 + // `wp.os.games.launch()` all land in `launchGame()`
862 + // with no hub window in the tab. Listing them here
863 + // costs a URL each in the boot config and no CSS
864 + // until `launchGame()` asks.
865 + function_exists( 'openstation_games_style_handles' )
866 + ? openstation_games_style_handles()
867 + : array()
868 + )
869 + ),
573 870 )
574 871 );
575 872
576 - wp_localize_script( 'desktop-mode', 'desktopModeConfig', $config );
873 + wp_localize_script( 'openstation', 'openStationConfig', $config );
577 874
578 875 /**
579 - * Fires when desktop mode assets are enqueued.
580 - *
581 - * @since 0.1.0
876 + * Fires when OpenStation assets are enqueued.
582 877 */
583 - do_action( 'desktop_mode_mode_init' );
878 + do_action( 'openstation_mode_init' );
584 879 }
585 -add_action( 'admin_enqueue_scripts', 'desktop_mode_enqueue_assets' );
880 +add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' );
586 881
587 882 /**
883 + * Keep Core's boot-time command-palette enqueue off shell pages.
884 + *
885 + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on
886 + * `admin_enqueue_scripts` by default, which puts the palette's whole
887 + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on
888 + * every admin page. On a SHELL page that is pure dead weight: the
889 + * shell suppresses Core's palette unconditionally (the ⌘K keystroke
890 + * and the admin-bar icon both route to the shell's own palette), so
891 + * the runtime it powers can never be shown. Unhooking here lets the
892 + * deferred manifest (`openstation_build_command_palette_assets_payload()`)
893 + * capture the chain instead, and the shell loads it on the first
894 + * palette invocation.
895 + *
896 + * Deliberately scoped: classic-mode requests keep Core's default,
897 + * because a classic page is Core's own UI where Core's palette is the
898 + * right one.
899 + *
900 + * Windows are handled separately by
901 + * {@see openstation_chromeless_should_trim_command_palette()} in
902 + * `includes/render/chromeless-trim.php` — same idea, but it has to
903 + * drop the whole palette *family* rather than just unhook Core's
904 + * callback, and it exempts block-editor screens. Unhooking alone is
905 + * not enough there: a third-party palette extension that declares
906 + * `wp-commands` keeps the entire chain queued as its dependency.
907 + *
908 + * Priority 0, ahead of Core's default 10, so the removal lands
909 + * before the callback fires. On WP 6.9 (function exists, no default
910 + * hook) the `remove_action()` is a harmless no-op.
911 + */
912 +function openstation_defer_core_command_palette() {
913 + if ( ! openstation_is_shell_request() ) {
914 + return;
915 + }
916 + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
917 +}
918 +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 );
919 +
920 +/**
588 921 * Emits `<link rel="preload">` hints for the shell's critical-path
589 922 * assets so the browser starts fetching them as soon as it parses
590 923 * the document `<head>`.
591 924 *
@@ -618,9 +951,9 @@
618 951 * later (often beyond the ~3s window Chrome allows a `preload` before it
619 952 * warns "preloaded but not used in time"), so `prefetch` keeps the early
620 953 * low-priority cache fill without the must-use-now contract.
621 954 *
622 - * Plugins can extend the hint list via the `desktop_mode_preload_hints`
955 + * Plugins can extend the hint list via the `openstation_preload_hints`
623 956 * filter — e.g. a settings tab whose bundle the user opens on every
624 957 * visit can opt its own URL into the preload phase.
625 958 *
626 959 * Same-origin resources only — no `crossorigin` attribute. CDN hosts
@@ -626,27 +959,20 @@
626 959 * Same-origin resources only — no `crossorigin` attribute. CDN hosts
627 960 * that serve `wp-content/plugins/` from a different origin should
628 961 * supply absolute URLs through the filter; in that case the consumer
629 962 * is responsible for the `crossorigin` semantics.
630 - *
631 - * @since 0.8.9
632 963 */
633 -function desktop_mode_print_preload_hints() {
634 - if (
635 - ! is_admin()
636 - || ! desktop_mode_is_enabled()
637 - || desktop_mode_is_chromeless_request()
638 - || desktop_mode_is_classic_request()
639 - ) {
964 +function openstation_print_preload_hints() {
965 + if ( ! openstation_is_shell_request() ) {
640 966 return;
641 967 }
642 968
643 - $suffix = desktop_mode_asset_suffix();
969 + $suffix = openstation_asset_suffix();
644 970
645 971 $build_url = static function ( $relative ) {
646 - $path = DESKTOP_MODE_DIR . $relative;
647 - $ver = file_exists( $path ) ? (string) filemtime( $path ) : DESKTOP_MODE_VERSION;
648 - return DESKTOP_MODE_URL . $relative . '?ver=' . $ver;
972 + $path = OPENSTATION_DIR . $relative;
973 + $ver = file_exists( $path ) ? (string) filemtime( $path ) : OPENSTATION_VERSION;
974 + return OPENSTATION_URL . $relative . '?ver=' . $ver;
649 975 };
650 976
651 977 $hints = array(
652 978 // Critical path — consumed on this very page load (the footer
@@ -680,8 +1006,21 @@
680 1006 'rel' => 'prefetch',
681 1007 ),
682 1008 );
683 1009
1010 + // The phone layer is needed at boot on a phone and never on a
1011 + // desktop; the server cannot see the viewport, so the user agent
1012 + // decides whether the hint is worth its bytes. A wrong guess costs
1013 + // one low-priority fetch, never a wrong layout — the stamp and the
1014 + // bundle loader read the real viewport.
1015 + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) {
1016 + $hints[] = array(
1017 + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ),
1018 + 'as' => 'script',
1019 + 'rel' => 'prefetch',
1020 + );
1021 + }
1022 +
684 1023 /**
685 1024 * Filters the list of resource preload hints emitted in `<head>`.
686 1025 *
687 1026 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -691,16 +1030,13 @@
691 1030 * back to `preload`. Unrecognized entries are silently skipped — keep
692 1031 * the contract permissive so a misconfigured plugin can't tank first
693 1032 * paint.
694 1033 *
695 - * @since 0.8.9
696 - * @since 0.9.1 Entries may carry a `rel` key (`preload` | `prefetch`).
697 - *
698 1034 * @param array $hints Default hints (main bundle + base CSS as
699 1035 * `preload`; window-system + shell-overlays as
700 1036 * `prefetch`).
701 1037 */
702 - $hints = apply_filters( 'desktop_mode_preload_hints', $hints );
1038 + $hints = apply_filters( 'openstation_preload_hints', $hints );
703 1039
704 1040 if ( ! is_array( $hints ) ) {
705 1041 return;
706 1042 }
@@ -728,12 +1064,12 @@
728 1064 esc_url( $href )
729 1065 );
730 1066 }
731 1067 }
732 -add_action( 'admin_print_styles', 'desktop_mode_print_preload_hints', 1 );
1068 +add_action( 'admin_print_styles', 'openstation_print_preload_hints', 1 );
733 1069
734 1070 /**
735 - * Defers loading of non-critical desktop-mode stylesheets so they
1071 + * Defers loading of non-critical openstation stylesheets so they
736 1072 * don't block first paint.
737 1073 *
738 1074 * Three stylesheets in the default enqueue list are only needed
739 1075 * after a user interaction — `dock-peek` (mouseover a dock tile),
@@ -757,14 +1093,12 @@
757 1093 * before the user clicks anything that needs them. The
758 1094 * `<noscript>` fallback restores critical-path behavior for JS-off
759 1095 * browsers, so accessibility isn't degraded.
760 1096 *
761 - * Filterable via `desktop_mode_deferred_styles` so plugins can opt
1097 + * Filterable via `openstation_deferred_styles` so plugins can opt
762 1098 * their own non-critical stylesheets in (or pull a built-in out).
763 1099 * Chromeless iframes are skipped — their CSS pipeline is separate.
764 1100 *
765 - * @since 0.8.9
766 - *
767 1101 * @param string $html The original <link> tag HTML.
768 1102 * @param string $handle The stylesheet handle WP is printing.
769 1103 * @param string $href The full URL of the stylesheet.
770 1104 * @param string $media The media attribute value WP resolved.
@@ -769,22 +1103,22 @@
769 1103 * @param string $href The full URL of the stylesheet.
770 1104 * @param string $media The media attribute value WP resolved.
771 1105 * @return string Possibly-rewritten tag.
772 1106 */
773 -function desktop_mode_defer_non_critical_styles( $html, $handle, $href, $media ) {
1107 +function openstation_defer_non_critical_styles( $html, $handle, $href, $media ) {
774 1108 // Cheap gates first — `style_loader_tag` fires once per enqueued
775 1109 // stylesheet on EVERY admin page (frontend doesn't go through
776 - // this filter, but admin does, including pages where desktop mode
777 - // is disabled). The deferred handles only ship when desktop mode
1110 + // this filter, but admin does, including pages where OpenStation
1111 + // is disabled). The deferred handles only ship when OpenStation
778 1112 // is active, so the in_array check below would always miss on
779 1113 // classic-only admin pages — but the `apply_filters` call still
780 1114 // builds an array and walks subscribers per stylesheet. Short-
781 1115 // circuit on the cheap helper checks (`is_admin` / enabled /
782 - // chromeless) so non-desktop-mode users pay nothing.
783 - if ( ! desktop_mode_is_enabled() ) {
1116 + // chromeless) so non-openstation users pay nothing.
1117 + if ( ! openstation_is_enabled() ) {
784 1118 return $html;
785 1119 }
786 - if ( desktop_mode_is_chromeless_request() ) {
1120 + if ( openstation_is_chromeless_request() ) {
787 1121 return $html;
788 1122 }
789 1123
790 1124 /**
@@ -793,20 +1127,18 @@
793 1127 * their own non-critical stylesheets here, or pull a built-in
794 1128 * out (e.g. a plugin that surfaces the AI assistant on every
795 1129 * page might want to keep its CSS critical-path).
796 1130 *
797 - * @since 0.8.9
798 - *
799 1131 * @param string[] $handles Default deferred handles.
800 1132 */
801 1133 $deferred = apply_filters(
802 - 'desktop_mode_deferred_styles',
1134 + 'openstation_deferred_styles',
803 1135 array(
804 - 'desktop-mode-dock-peek',
1136 + 'os-dock-peek',
1137 + 'os-openstation-layout',
805 1138 'desktop-mode-ai-assistant',
806 1139 'desktop-mode-bug-report',
807 - 'desktop-mode-window-overview',
808 - 'desktop-mode-os-settings',
1140 + 'os-window-overview',
809 1141 )
810 1142 );
811 1143
812 1144 if ( ! in_array( $handle, (array) $deferred, true ) ) {
@@ -817,22 +1149,22 @@
817 1149 $id = $handle . '-css';
818 1150
819 1151 // Two contexts, two escapers for the same `$resolved_media` value:
820 1152 //
821 - // - `%3$s` lands inside a JS string literal inside the HTML
822 - // `onload="…"` attribute (`this.media='%3$s'`). `esc_attr`
823 - // escapes `"` and `&` but NOT single quotes, so a media
824 - // value containing `'` would break out of the JS string.
825 - // `esc_js` is the correct escaper for "string literal inside
826 - // an event-handler attribute" — escapes single quotes, double
827 - // quotes, backslashes, newlines. Today `$resolved_media`
828 - // comes from `wp_enqueue_style()`'s `$media` parameter (always
829 - // a CSS media type / query produced by WordPress core), so
830 - // this is pure defense-in-depth, but the cost is one extra
831 - // function call.
1153 + // - `%3$s` lands inside a JS string literal inside the HTML
1154 + // `onload="…"` attribute (`this.media='%3$s'`). `esc_attr`
1155 + // escapes `"` and `&` but NOT single quotes, so a media
1156 + // value containing `'` would break out of the JS string.
1157 + // `esc_js` is the correct escaper for "string literal inside
1158 + // an event-handler attribute" — escapes single quotes, double
1159 + // quotes, backslashes, newlines. Today `$resolved_media`
1160 + // comes from `wp_enqueue_style()`'s `$media` parameter (always
1161 + // a CSS media type / query produced by WordPress core), so
1162 + // this is pure defense-in-depth, but the cost is one extra
1163 + // function call.
832 1164 //
833 - // - `%4$s` lands inside an HTML attribute in the `<noscript>`
834 - // fallback (`media='%4$s'`). That's standard `esc_attr`.
1165 + // - `%4$s` lands inside an HTML attribute in the `<noscript>`
1166 + // fallback (`media='%4$s'`). That's standard `esc_attr`.
835 1167 //
836 1168 // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- This filter rewrites a tag WordPress is in the process of emitting for an already-registered+enqueued stylesheet handle; the linter doesn't trace the `style_loader_tag` filter context, so the raw <link rel="stylesheet"> output is a false-positive.
837 1169 $markup = sprintf(
838 1170 '<link rel=\'stylesheet\' id=\'%1$s\' href=\'%2$s\' media=\'print\' onload="this.media=\'%3$s\'; this.onload=null;" />' . "\n" .
@@ -845,19 +1177,19 @@
845 1177 // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
846 1178
847 1179 return $markup;
848 1180 }
849 -add_filter( 'style_loader_tag', 'desktop_mode_defer_non_critical_styles', 10, 4 );
1181 +add_filter( 'style_loader_tag', 'openstation_defer_non_critical_styles', 10, 4 );
850 1182
851 1183 /**
852 1184 * Build the admin-menu command map (name → URL) and expose it on
853 - * `window.__desktopModeMenuCommands`. The shell command harvester
1185 + * `window.__openStationMenuCommands`. The shell command harvester
854 1186 * (`src/commands/shell-harvester.ts`) reads this slot to resolve URLs
855 1187 * for "Go to: …" commands whose JS callbacks
856 1188 * (`document.location = menuCommand.url`) close over a variable URL
857 1189 * we can't extract from source. Without this map those commands
858 1190 * either get skipped (no URL recoverable) or — if the location
859 - * shadow misses — navigate the SHELL out of desktop mode.
1191 + * shadow misses — navigate the SHELL out of OpenStation.
860 1192 *
861 1193 * Mirrors what WordPress core's `wp_enqueue_command_palette_assets()`
862 1194 * builds for `wp.coreCommands.initializeCommandPalette(...)`. We
863 1195 * duplicate the logic here (instead of monkey-patching the JS init
@@ -866,15 +1198,13 @@
866 1198 * same handle) and ship the result through `wp_add_inline_script` on
867 1199 * our own bundle handle. That decouples us entirely from WP's command-
868 1200 * palette mount timing.
869 1201 *
870 - * @since 0.8.4
871 - *
872 1202 * @global array $menu
873 1203 * @global array $submenu
874 1204 * @return array<int, array{label:string, url:string, name:string}>
875 1205 */
876 -function desktop_mode_build_command_menu_map() {
1206 +function openstation_build_command_menu_map() {
877 1207 global $menu, $submenu, $_parent_pages;
878 1208 if ( ! is_array( $menu ) ) {
879 1209 return array();
880 1210 }
@@ -927,9 +1257,9 @@
927 1257 // exception is URL-style slugs referencing a real admin file
928 1258 // (ACF's 'edit.php?post_type=acf-field-group' — also a
929 1259 // registered page) — those stay direct links, matching
930 1260 // classic admin's menu-header.php.
931 - if ( ( ! isset( $_parent_pages[ $menu_slug ] ) || desktop_mode_is_admin_file_slug( $menu_slug ) ) && ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) ) {
1261 + if ( ( ! isset( $_parent_pages[ $menu_slug ] ) || openstation_is_admin_file_slug( $menu_slug ) ) && ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) ) {
932 1262 $menu_url = $menu_slug;
933 1263 } elseif ( ! empty( menu_page_url( $menu_slug, false ) ) ) {
934 1264 $menu_url = menu_page_url( $menu_slug, false );
935 1265 }
@@ -952,9 +1282,9 @@
952 1282 $submenu_slug = $submenu_item[2];
953 1283 $submenu_url = '';
954 1284 // Same registered-page vs admin-file rule as the
955 1285 // top-level loop.
956 - if ( ( ! isset( $_parent_pages[ $submenu_slug ] ) || desktop_mode_is_admin_file_slug( $submenu_slug ) ) && ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) ) {
1286 + if ( ( ! isset( $_parent_pages[ $submenu_slug ] ) || openstation_is_admin_file_slug( $submenu_slug ) ) && ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) ) {
957 1287 $submenu_url = $submenu_slug;
958 1288 } elseif ( ! empty( menu_page_url( $submenu_slug, false ) ) ) {
959 1289 $submenu_url = menu_page_url( $submenu_slug, false );
960 1290 }