PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
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 0.8.8 All 35 releases
← All changes | includes/render/assets.php +389 -102 1.0.01.1.11 View file →
@@ -73,24 +73,76 @@
73 73 do_action( 'openstation_chromeless_styles' );
74 74 return;
75 75 }
76 76
77 - if ( ! openstation_is_enabled() || openstation_is_classic_request() ) {
77 + if ( ! openstation_is_shell_request() ) {
78 78 return;
79 79 }
80 80
81 - // CSS.
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.
82 87 wp_enqueue_style( 'openstation' );
83 88 wp_enqueue_style( 'os-windows' );
84 89 wp_enqueue_style( 'os-window-overview' );
85 - wp_enqueue_style( 'os-settings' );
86 90 wp_enqueue_style( 'os-dock' );
87 91 wp_enqueue_style( 'os-dock-peek' );
88 - wp_enqueue_style( 'desktop-mode-ai-assistant' );
89 - wp_enqueue_style( 'desktop-mode-bug-report' );
92 + wp_enqueue_style( 'os-notch' );
93 + wp_enqueue_style( 'os-workspaces' );
94 + wp_enqueue_style( 'os-shortcuts' );
95 + wp_enqueue_style( 'os-openstation-layout' );
90 96 wp_enqueue_style( 'os-files' );
91 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 +
93 145 // The rebrand announcement paints on one visit per user and never
94 146 // again, so its stylesheet is only worth sending to the users who
95 147 // are actually going to see it. Computed once here and reused for
96 148 // the `rebrandNotice` config key below, which reads the same answer.
@@ -115,35 +167,35 @@
115 167 // WP normally only calls this on screens that opt in to the native
116 168 // palette; the shell needs it on every admin URL it might wrap.
117 169 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
118 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 +
119 186 if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
120 - // `wp_enqueue_command_palette_assets()` calls
121 - // `array_key_exists( $menu_slug, $submenu )` without guarding
122 - // the global, so an unset `$submenu` (test contexts, edge-case
123 - // admin requests where the menu wasn't built yet) blows up
124 - // with a TypeError. Initialize defensively before calling.
125 - global $menu, $submenu;
126 - // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- initializing an unset global to its documented empty shape, not replacing a built menu.
127 - if ( ! isset( $submenu ) || ! is_array( $submenu ) ) {
128 - $submenu = array();
129 - }
130 - if ( ! isset( $menu ) || ! is_array( $menu ) ) {
131 - $menu = array();
132 - }
133 - // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
134 - wp_enqueue_command_palette_assets();
135 -
136 187 // Expose the same menu-commands array WP serializes into
137 188 // `wp.coreCommands.initializeCommandPalette(...)` on a window
138 189 // slot the shell harvester can read. Built in PHP from `$menu`
139 - // / `$submenu` here (we already guarded that they're arrays
140 - // above), then injected as a `before` inline on our own bundle
141 - // — that runs synchronously before `desktop.min.js` boots the
142 - // shell harvester, so the lookup is guaranteed populated by
143 - // the time `src/commands/shell-harvester.ts` classifies any
144 - // command. Decoupled from WP's command-palette mount timing
145 - // (which fires from a core-registered hook we can't reorder).
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.
146 198 $menu_map = openstation_build_command_menu_map();
147 199 wp_add_inline_script(
148 200 'openstation',
149 201 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
@@ -151,9 +203,9 @@
151 203 );
152 204 }
153 205
154 206 // Pass configuration to JavaScript.
155 - global $title, $pagenow, $parent_file, $menu;
207 + global $title, $parent_file, $menu;
156 208
157 209 $menu_icon = 'dashicons-admin-generic';
158 210 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
159 211 foreach ( $menu as $item ) {
@@ -169,13 +221,32 @@
169 221 // plugin-contributed top-level routes. `openstation_dock_placement`
170 222 // is the per-item filter escape hatch for hiding. Shared with the
171 223 // REST menu endpoint so live refreshes (post plugin-activation)
172 224 // produce the same ordering as the boot payload.
173 - $menu_payload = openstation_build_menu_payload();
174 - $dock_items = $menu_payload['dockItems'];
175 - $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'] )
176 228 ? $menu_payload['nativeWindows']
177 229 : array();
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();
178 249 $server_widgets = isset( $menu_payload['serverWidgets'] )
179 250 ? $menu_payload['serverWidgets']
180 251 : array();
181 252 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
@@ -198,8 +269,11 @@
198 269 : array();
199 270 $server_titlebar_button_scripts = isset( $menu_payload['serverTitleBarButtonScripts'] )
200 271 ? $menu_payload['serverTitleBarButtonScripts']
201 272 : array();
273 + $server_window_action_scripts = isset( $menu_payload['serverWindowActionScripts'] )
274 + ? $menu_payload['serverWindowActionScripts']
275 + : array();
202 276 $server_window_theme_scripts = isset( $menu_payload['serverWindowThemeScripts'] )
203 277 ? $menu_payload['serverWindowThemeScripts']
204 278 : array();
205 279 $server_window_themes = isset( $menu_payload['serverWindowThemes'] )
@@ -239,9 +313,29 @@
239 313 // dedupes on an `activeId` that was never set.
240 314 $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
241 315 ? $menu_payload['serverDesktopThemes']
242 316 : array();
243 - $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'] )
244 338 ? $menu_payload['desktopIcons']
245 339 : array();
246 340
247 341 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -252,8 +346,16 @@
252 346 : array();
253 347 $server_file_openers = function_exists( 'openstation_build_file_openers_payload' )
254 348 ? openstation_build_file_openers_payload()
255 349 : array();
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']
357 + : array();
256 358 $user_file_associations = function_exists( 'openstation_get_user_file_associations' )
257 359 ? openstation_get_user_file_associations( get_current_user_id() )
258 360 : array();
259 361 $server_wallpaper_menu_items = function_exists( 'openstation_build_wallpaper_menu_items' )
@@ -318,9 +420,9 @@
318 420 'maxSize' => $drop_max_size,
319 421 );
320 422
321 423 // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant,
322 - // About-scene, OS Settings panel, shell-overlays, window-system)
424 + // OS Settings panel, shell-overlays, window-system)
323 425 // is `<script>`-injected by the main bundle on demand — they don't
324 426 // go through `wp_register_script`, so they don't pick up WordPress's
325 427 // usual `?ver=<filemtime>` cache-buster. Without one, the browser
326 428 // happily serves a stale cached copy across plugin updates that
@@ -342,18 +444,33 @@
342 444 OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
343 445 );
344 446 };
345 447
346 - // Build the current page URL from $pagenow + $_GET. Strip the portal
347 - // markers so the derived window ID matches what the dock would produce
348 - // for the same page — otherwise auto-opening the entry window and
349 - // clicking the same dock icon would create a duplicate.
350 - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
351 - unset( $current_query[ OPENSTATION_PORTAL_FLAG ], $current_query[ OPENSTATION_PORTAL_INTENT_FLAG ] );
352 - $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'];
353 459
354 - $from_portal = ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
355 - $from_portal_intent = ! empty( $_GET[ OPENSTATION_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 + }
356 473
357 474 /**
358 475 * Filters the desktop shell configuration passed to JavaScript.
359 476 *
@@ -385,16 +502,17 @@
385 502 * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference.
386 503 * @type bool $canUpload Whether the user holds the `upload_files` capability.
387 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.
388 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.
389 507 * @type string $restNonce Nonce for the session REST endpoint.
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.
390 509 * @type string $portalUrl Canonical `/openstation/` URL.
391 510 * @type bool $fromPortal Whether the shell was reached via the portal.
392 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.
393 - * @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.
512 + * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`).
394 513 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
395 514 * @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.
396 - * @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 `openstation_sticky_notes_is_available()`.
397 515 * }
398 516 */
399 517 $config = apply_filters(
400 518 'openstation_shell_config',
@@ -399,11 +517,22 @@
399 517 $config = apply_filters(
400 518 'openstation_shell_config',
401 519 array(
402 520 'currentPage' => esc_url( $current_page ),
403 - 'currentTitle' => wp_strip_all_tags( $title ),
521 + 'currentTitle' => $current_title,
404 522 'currentIcon' => sanitize_html_class( $menu_icon ),
405 - 'adminUrl' => esc_url( admin_url() ),
523 + // `self_admin_url()`: the base a window id is derived from,
524 + // and the URL the shell leaves for on exit. Both want the
525 + // admin the screen is in, which is the network one when the
526 + // network shell screen is what rendered.
527 + 'adminUrl' => esc_url( self_admin_url() ),
528 + 'homeUrl' => esc_url( home_url( '/' ) ),
529 + // Decoded: the shell assigns this to `window.location`,
530 + // where `&amp;` would make `_wpnonce` arrive as
531 + // `amp;_wpnonce` and fail the nonce check.
532 + 'logoutUrl' => esc_url_raw(
533 + html_entity_decode( wp_logout_url(), ENT_QUOTES, 'UTF-8' )
534 + ),
406 535 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ),
407 536 'dockItems' => $dock_items,
408 537 // Baseline menu fingerprint. The shell seeds its last-known
409 538 // signature from this so the first off-allowlist menu change
@@ -409,8 +538,12 @@
409 538 // signature from this so the first off-allowlist menu change
410 539 // (vs. this boot state) is caught without a wasted probe. GH#325.
411 540 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
412 541 'nativeWindows' => $native_windows,
542 + // Handle-keyed script data the entries above reference —
543 + // one copy per bundle, not one per window. See
544 + // `openstation_collect_native_windows_payload()`.
545 + 'nativeWindowScriptData' => $native_window_script_data,
413 546 'serverWidgets' => $server_widgets,
414 547 'serverWallpapers' => $server_wallpapers,
415 548 'serverCommandScripts' => $server_command_scripts,
416 549 'serverCommands' => $server_commands,
@@ -417,8 +550,9 @@
417 550 'serverSettingsTabScripts' => $server_settings_tab_scripts,
418 551 'serverSettingsTabs' => $server_settings_tabs,
419 552 'serverDockRailRendererScripts' => $server_dock_rail_renderer_scripts,
420 553 'serverTitleBarButtonScripts' => $server_titlebar_button_scripts,
554 + 'serverWindowActionScripts' => $server_window_action_scripts,
421 555 'serverWindowThemeScripts' => $server_window_theme_scripts,
422 556 'serverWindowThemes' => $server_window_themes,
423 557 'serverWindowControlScripts' => $server_window_control_scripts,
424 558 'serverWindowControls' => $server_window_controls,
@@ -435,8 +569,11 @@
435 569 'serverDesktopThemes' => $server_desktop_themes,
436 570 'desktopIcons' => $desktop_icons,
437 571 'serverFileTypes' => $server_file_types,
438 572 'serverFileOpeners' => $server_file_openers,
573 + // Handle => dependency payload for every `scriptDeps` list in
574 + // this config; see `openstation_compact_script_deps()`.
575 + 'scriptDepPayloads' => (object) $script_dep_payloads,
439 576 'userFileAssociations' => $user_file_associations,
440 577 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
441 578 // Pinned-notes REST base (`includes/notes/rest.php`). The
442 579 // notes layer boots only when this is present.
@@ -452,9 +589,17 @@
452 589 'coreNotices' => openstation_get_core_notices(),
453 590 'pluginNotices' => openstation_get_plugin_notices(),
454 591 'defaultWallpaper' => openstation_get_default_wallpaper(),
455 592 'session' => openstation_get_session( get_current_user_id() ),
456 - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
593 + // The session route runs in the main site's blog context
594 + // whichever desktop posts to it, so the network screen's
595 + // URL says which session it is addressing — see
596 + // `openstation_rest_session_network()`.
597 + 'sessionUrl' => esc_url_raw(
598 + is_network_admin()
599 + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) )
600 + : rest_url( 'desktop-mode/v1/session' )
601 + ),
457 602 'restUrl' => esc_url_raw( rest_url() ),
458 603 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
459 604 'dropConfig' => $drop_config,
460 605 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
@@ -461,8 +606,17 @@
461 606 'defaultWindow' => openstation_get_default_window( get_current_user_id() ),
462 607 'canUpload' => current_user_can( 'upload_files' ),
463 608 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ),
464 609 'pluginVersion' => OPENSTATION_VERSION,
610 + 'aboutFeedUrl' => esc_url_raw(
611 + add_query_arg(
612 + array(
613 + 'action' => 'openstation_about_feed',
614 + 'nonce' => wp_create_nonce( 'openstation_about_feed' ),
615 + ),
616 + admin_url( 'admin-ajax.php' )
617 + )
618 + ),
465 619 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
466 620 // URL of the AI Assistant lazy bundle. The main bundle
467 621 // ships a stub matching the public `wp.os.ai` API; the
468 622 // stub `<script>`-injects this URL the first time the user
@@ -468,25 +622,39 @@
468 622 // stub `<script>`-injects this URL the first time the user
469 623 // opens the assistant. Picking `.js` vs `.min.js` here keeps
470 624 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
471 625 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
472 - // URL of the About-scene lazy bundle. The OS Settings →
473 - // About tab loads this on first mount; ~25 kB PixiJS
474 - // particle scene that would otherwise ship in the main
475 - // bundle for every shell load.
476 - 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ),
477 - // URL of the OS Settings panel lazy bundle. Injected by
478 - // the main bundle's `OsSettings.renderPanel()` stub on
479 - // the user's first Settings open. Holds every section
480 - // renderer + the `<os-*>` components only the panel
481 - // uses, so nothing about Settings ships in
482 - // `desktop.min.js` for users who never open it.
483 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
484 626 // URL of the shell-overlays lazy bundle. Pre-loaded by
485 627 // the main bundle after first paint so action-triggered
486 628 // overlays (toast, confirm dialog, context menus) feel
487 629 // instant the first time they fire.
488 630 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
631 + // The shell-bundle diet: features whose right moment is a
632 + // user gesture (or a presence signal) ride their own
633 + // bundles instead of the boot-critical `desktop[.min].js`.
634 + // Each sentinel in the shell loads its bundle at that
635 + // moment — see the entry file each bundle names.
636 + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ),
637 + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ),
638 + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ),
639 + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ),
640 + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ),
641 + // Presence hint for the notes sentinel: a desktop with no
642 + // notes skips the notes bundle AND the boot-time list
643 + // request. Two id-only existence probes at most.
644 + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' )
645 + ? openstation_notes_user_has_any()
646 + : false,
647 + // URL of the full `<os-*>` component kit. The shell
648 + // never loads this — its own bundles import the
649 + // components they render. It exists for
650 + // `wp.os.loadComponents()`, i.e. for plugin code that
651 + // CANNOT import: a plugin shipped as a zip has no path
652 + // to this repo at build time, so before this URL its
653 + // only routes to a `<os-switch>` were to bundle a second
654 + // copy or hand-roll one. Shipping the URL costs one
655 + // string and keeps the SCRIPT_DEBUG choice server-side.
656 + 'componentsBundleUrl' => $lazy_bundle_url( 'os-components' ),
489 657 // Mio — the desk companion. `mio` carries the
490 658 // appearance + physics (see `openstation_mio_config()`);
491 659 // `mioBundleUrl` is the lazy PixiJS bundle the shell
492 660 // controller injects the first time a user switches the
@@ -510,17 +678,37 @@
510 678 // / `openNew()` call (both async); pre-loaded
511 679 // by the shell after first paint when no session is being
512 680 // restored and no `openCurrentPage` will fire.
513 681 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
682 + // URL of the lazy phone-layer bundle. Injected by the main
683 + // bundle only when the mode resolves to `mobile`, so a
684 + // desktop never fetches it. `mode` carries the preference
685 + // and breakpoints the first-paint head stamp already used,
686 + // plus the server's default tab-bar pins — see
687 + // `includes/mobile.php`.
688 + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ),
689 + 'mode' => openstation_mode_config( get_current_user_id() ),
514 690 // URL of the item-visibility-menu lazy bundle — the
515 691 // right-click "hide from dock / desktop" menu. Injected by
516 692 // the main bundle's loader shim on the first right-click.
517 693 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
694 + // URL of the workspace-wizard lazy bundle — the modal
695 + // behind "Edit this workspace…". Injected by the main
696 + // bundle's loader shim on first open.
697 + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ),
698 + // Server-side view of the workspace templates, so a plugin
699 + // can add or drop one from PHP. The client merges these
700 + // with its own built-ins by id — see
701 + // `src/workspaces/server-sync.ts`.
702 + 'workspacePresets' => openstation_workspace_presets(),
518 703 // URL of the release-card lazy bundle — the vinyl core-
519 704 // update announcement. Injected by `maybeShowUpdate()` only
520 705 // when a core update is actually pending.
521 706 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
522 707 'restNonce' => wp_create_nonce( 'wp_rest' ),
708 + // Non-empty when the shell was asked to paint exactly one
709 + // window and nothing else. See `OPENSTATION_SOLO_FLAG`.
710 + 'soloWindow' => openstation_solo_window_id(),
523 711 'osSettings' => openstation_get_os_settings( get_current_user_id() ),
524 712 'osSettingsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/os-settings' ) ),
525 713 'seenIntros' => openstation_get_seen_intros( get_current_user_id() ),
526 714 'seenIntrosUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/intros' ) ),
@@ -529,18 +717,9 @@
529 717 // announcement yet. Same value that gated `os-announce`
530 718 // above; the dialog cannot paint without that stylesheet, so
531 719 // the two must not diverge.
532 720 'rebrandNotice' => $show_rebrand_notice,
533 - // Sticky notes ride on Gutenberg's Guidelines experiment
534 - // (the `wp_guideline` CPT + `wp_guideline_type` taxonomy).
535 - // When that experiment isn't active the `wp/v2/guidelines`
536 - // + `wp/v2/wp_guideline_type` probes 404 — harmless but
537 - // noisy — so the shell skips booting the layer entirely.
538 - 'stickyNotes' => array(
539 - 'available' => openstation_sticky_notes_is_available(),
540 - ),
541 721 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
542 - 'aiSearchStreamUrl' => esc_url_raw( add_query_arg( 'action', 'openstation_ai_search_stream', admin_url( 'admin-ajax.php' ) ) ),
543 722 // AI assistant availability + per-user toggle. Drives whether the
544 723 // Cmd+K palette and admin-bar icon appear, and the setup placeholder.
545 724 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' )
546 725 ? openstation_ai_assistant_config()
@@ -553,41 +732,57 @@
553 732 // Site-wide games kill switch (Extended options). Exposed to
554 733 // every user — the shell skips the challenges Heartbeat
555 734 // channel when the framework is off.
556 735 'gamesEnabled' => openstation_games_enabled(),
557 - // Comments-window AI moderation toggle — surfaced at the
558 - // shell level so the OS Settings → Features tab can render
559 - // the toggle without depending on the Comments window
560 - // being registered for this user. URL is the same
561 - // endpoint the comments-window config exposes; state is
562 - // `null` for non-admins (the UI hides the row entirely).
563 - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
564 - // Non-null only for admins on a site where the Core AI stack is
565 - // present. Comment scoring routes through the AI Client (WP 7.0+),
566 - // so on older WordPress the whole row is hidden — same as the
567 - // assistant toggle — rather than shown disabled pointing at a
568 - // Settings → Connectors screen that doesn't exist there.
569 - 'commentsAi' => (
570 - current_user_can( 'manage_options' )
571 - && function_exists( 'openstation_ai_is_available' )
572 - && openstation_ai_is_available()
573 - )
574 - ? array(
575 - 'enabled' => function_exists( 'openstation_comments_ai_is_enabled' )
576 - ? openstation_comments_ai_is_enabled()
577 - : false,
578 - 'providerConfigured' => function_exists( 'openstation_comments_ai_provider_configured' )
579 - ? openstation_comments_ai_provider_configured()
580 - : false,
581 - )
582 - : null,
583 736 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
737 + // Null on single-site installs; its `networkAdmin` null
738 + // without `manage_network`, which is what keeps the dock
739 + // tile from registering.
740 + 'multisite' => openstation_multisite_payload(),
584 741 'portalUrl' => esc_url( openstation_portal_url() ),
585 742 'fromPortal' => $from_portal,
586 743 'fromPortalIntent' => $from_portal_intent,
744 + // One-shot like the boot target: a switch from another
745 + // site's overview lands in this one's.
746 + 'landInOverview' => openstation_shell_lands_in_overview(),
747 + 'arrivalDirection' => openstation_shell_arrival_direction(),
748 + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null,
587 749 'pwa' => array(
588 750 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
589 751 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ),
752 + // Extensionless retry target for hosts whose nginx 404s
753 + // virtual .js paths before WordPress runs (WordPress.com).
754 + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ),
755 + // The site's home path — the scope the registration
756 + // asks for, so a subdirectory network's sites each get
757 + // their own worker instead of fighting over the root.
758 + 'swScope' => openstation_pwa_sw_scope(),
759 + // The worker's per-user flags, computed HERE rather than
760 + // baked into the served `sw.js`.
761 + //
762 + // A service worker is origin-wide but these are per-user
763 + // preferences, so putting them in the script bytes made
764 + // the body differ between an anonymous and a logged-in
765 + // request — and any in-scope logged-out navigation then
766 + // installed a "new" worker, which at the time reloaded
767 + // the shell. The bytes are identical for everyone now;
768 + // the shell posts these to the worker at boot.
769 + //
770 + // Computed server-side, not read from the settings
771 + // snapshot client-side, because
772 + // `openstation_pwa_admin_asset_cache_enabled()` applies
773 + // the `openstation_pwa_admin_asset_cache` filter — an
774 + // operator's site-wide veto has to keep working.
775 + 'swConfig' => array(
776 + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(),
777 + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ),
778 + ),
779 + // The build this shell document belongs to. When a new
780 + // worker takes over mid-session the shell asks it for
781 + // the stamp it was served with and compares; only a
782 + // difference — the shell's own files changed on the
783 + // server — offers the user a reload. Never automatic.
784 + 'shellBuild' => openstation_shell_build_stamp(),
590 785 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
591 786 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
592 787 // Mirrors the manifest's `name` field — used by the
593 788 // install pill so the button reads "Install <site>"
@@ -603,8 +798,54 @@
603 798 // `src/pwa/sw-register.ts`). Default `false` preserves
604 799 // the polite behaviour where we yield to existing PWAs.
605 800 'forceReplaceSw' => openstation_pwa_force_replace_sw(),
606 801 ),
802 + // Ordered Core command-palette asset manifest, replayed on
803 + // first palette invocation. `null` on pre-6.9 sites.
804 + 'commandPalette' => $command_palette,
805 + // Stylesheets for shell surfaces that render on demand —
806 + // the Preferences panel, the AI assistant, the bug-report
807 + // window. None of them is a server-registered native
808 + // window (they are built client-side by the shell
809 + // bundle), so the `styles` companion mechanism can't
810 + // carry their CSS; instead the shell injects each sheet
811 + // the first time its surface opens, via
812 + // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
813 + // Same resolved shape a native window's `styleUrl` /
814 + // `styleInline` travels in.
815 + // Which of the `deferredStyles` entries a game needs.
816 + // `launchGame()` injects these before the window paints.
817 + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' )
818 + ? openstation_games_style_handles()
819 + : array(),
820 + 'deferredStyles' => openstation_build_deferred_styles(
821 + array_merge(
822 + array(
823 + 'desktop-mode-ai-assistant',
824 + 'desktop-mode-bug-report',
825 + // The explorer's shared sheet. It rides the WP
826 + // Explorer APP as a companion style, but the
827 + // desktop FOLDER window paints its preview pane
828 + // with the same `os-my-wordpress__*` classes and
829 + // — being a native window opened straight from
830 + // JS — carries no companion styles of its own.
831 + // Without this, the pane rendered unstyled until
832 + // the explorer had been opened once in the
833 + // session.
834 + 'desktop-mode-my-wordpress',
835 + ),
836 + // The Games sheets. They also ride the hub window as
837 + // companion styles, but a game is reachable without
838 + // the hub — the challenge toast, solo mode, and
839 + // `wp.os.games.launch()` all land in `launchGame()`
840 + // with no hub window in the tab. Listing them here
841 + // costs a URL each in the boot config and no CSS
842 + // until `launchGame()` asks.
843 + function_exists( 'openstation_games_style_handles' )
844 + ? openstation_games_style_handles()
845 + : array()
846 + )
847 + ),
607 848 )
608 849 );
609 850
610 851 wp_localize_script( 'openstation', 'openStationConfig', $config );
@@ -616,8 +857,46 @@
616 857 }
617 858 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' );
618 859
619 860 /**
861 + * Keep Core's boot-time command-palette enqueue off shell pages.
862 + *
863 + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on
864 + * `admin_enqueue_scripts` by default, which puts the palette's whole
865 + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on
866 + * every admin page. On a SHELL page that is pure dead weight: the
867 + * shell suppresses Core's palette unconditionally (the ⌘K keystroke
868 + * and the admin-bar icon both route to the shell's own palette), so
869 + * the runtime it powers can never be shown. Unhooking here lets the
870 + * deferred manifest (`openstation_build_command_palette_assets_payload()`)
871 + * capture the chain instead, and the shell loads it on the first
872 + * palette invocation.
873 + *
874 + * Deliberately scoped: classic-mode requests keep Core's default,
875 + * because a classic page is Core's own UI where Core's palette is the
876 + * right one.
877 + *
878 + * Windows are handled separately by
879 + * {@see openstation_chromeless_should_trim_command_palette()} in
880 + * `includes/render/chromeless-trim.php` — same idea, but it has to
881 + * drop the whole palette *family* rather than just unhook Core's
882 + * callback, and it exempts block-editor screens. Unhooking alone is
883 + * not enough there: a third-party palette extension that declares
884 + * `wp-commands` keeps the entire chain queued as its dependency.
885 + *
886 + * Priority 0, ahead of Core's default 10, so the removal lands
887 + * before the callback fires. On WP 6.9 (function exists, no default
888 + * hook) the `remove_action()` is a harmless no-op.
889 + */
890 +function openstation_defer_core_command_palette() {
891 + if ( ! openstation_is_shell_request() ) {
892 + return;
893 + }
894 + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
895 +}
896 +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 );
897 +
898 +/**
620 899 * Emits `<link rel="preload">` hints for the shell's critical-path
621 900 * assets so the browser starts fetching them as soon as it parses
622 901 * the document `<head>`.
623 902 *
@@ -660,14 +939,9 @@
660 939 * supply absolute URLs through the filter; in that case the consumer
661 940 * is responsible for the `crossorigin` semantics.
662 941 */
663 942 function openstation_print_preload_hints() {
664 - if (
665 - ! is_admin()
666 - || ! openstation_is_enabled()
667 - || openstation_is_chromeless_request()
668 - || openstation_is_classic_request()
669 - ) {
943 + if ( ! openstation_is_shell_request() ) {
670 944 return;
671 945 }
672 946
673 947 $suffix = openstation_asset_suffix();
@@ -710,8 +984,21 @@
710 984 'rel' => 'prefetch',
711 985 ),
712 986 );
713 987
988 + // The phone layer is needed at boot on a phone and never on a
989 + // desktop; the server cannot see the viewport, so the user agent
990 + // decides whether the hint is worth its bytes. A wrong guess costs
991 + // one low-priority fetch, never a wrong layout — the stamp and the
992 + // bundle loader read the real viewport.
993 + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) {
994 + $hints[] = array(
995 + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ),
996 + 'as' => 'script',
997 + 'rel' => 'prefetch',
998 + );
999 + }
1000 +
714 1001 /**
715 1002 * Filters the list of resource preload hints emitted in `<head>`.
716 1003 *
717 1004 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -824,12 +1111,12 @@
824 1111 $deferred = apply_filters(
825 1112 'openstation_deferred_styles',
826 1113 array(
827 1114 'os-dock-peek',
1115 + 'os-openstation-layout',
828 1116 'desktop-mode-ai-assistant',
829 1117 'desktop-mode-bug-report',
830 1118 'os-window-overview',
831 - 'os-settings',
832 1119 )
833 1120 );
834 1121
835 1122 if ( ! in_array( $handle, (array) $deferred, true ) ) {