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