PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
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 0.8.7 All 34 releases
← All changes | includes/render/assets.php +223 -67 1.1.31.1.10 View file →
@@ -73,9 +73,9 @@
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 81 // CSS. Only the sheets that paint surfaces present at boot — the
@@ -89,12 +89,16 @@
89 89 wp_enqueue_style( 'os-window-overview' );
90 90 wp_enqueue_style( 'os-dock' );
91 91 wp_enqueue_style( 'os-dock-peek' );
92 92 wp_enqueue_style( 'os-notch' );
93 + wp_enqueue_style( 'os-workspaces' );
93 94 wp_enqueue_style( 'os-shortcuts' );
94 95 wp_enqueue_style( 'os-openstation-layout' );
95 96 wp_enqueue_style( 'os-files' );
96 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' );
97 101
98 102 // Solo mode — a single window freed into a native OS window by the
99 103 // desktop host. Same shell, everything but that one window hidden.
100 104 $solo_window = openstation_solo_window_id();
@@ -199,9 +203,9 @@
199 203 );
200 204 }
201 205
202 206 // Pass configuration to JavaScript.
203 - global $title, $pagenow, $parent_file, $menu;
207 + global $title, $parent_file, $menu;
204 208
205 209 $menu_icon = 'dashicons-admin-generic';
206 210 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
207 211 foreach ( $menu as $item ) {
@@ -217,13 +221,32 @@
217 221 // plugin-contributed top-level routes. `openstation_dock_placement`
218 222 // is the per-item filter escape hatch for hiding. Shared with the
219 223 // REST menu endpoint so live refreshes (post plugin-activation)
220 224 // produce the same ordering as the boot payload.
221 - $menu_payload = openstation_build_menu_payload();
222 - $dock_items = $menu_payload['dockItems'];
223 - $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'] )
224 228 ? $menu_payload['nativeWindows']
225 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();
226 249 $server_widgets = isset( $menu_payload['serverWidgets'] )
227 250 ? $menu_payload['serverWidgets']
228 251 : array();
229 252 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
@@ -290,9 +313,29 @@
290 313 // dedupes on an `activeId` that was never set.
291 314 $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
292 315 ? $menu_payload['serverDesktopThemes']
293 316 : array();
294 - $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'] )
295 338 ? $menu_payload['desktopIcons']
296 339 : array();
297 340
298 341 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -393,18 +436,33 @@
393 436 OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
394 437 );
395 438 };
396 439
397 - // Build the current page URL from $pagenow + $_GET. Strip the portal
398 - // markers so the derived window ID matches what the dock would produce
399 - // for the same page — otherwise auto-opening the entry window and
400 - // clicking the same dock icon would create a duplicate.
401 - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
402 - unset( $current_query[ OPENSTATION_PORTAL_FLAG ], $current_query[ OPENSTATION_PORTAL_INTENT_FLAG ] );
403 - $current_page = admin_url( $pagenow ) . ( ! empty( $current_query ) ? '?' . http_build_query( $current_query ) : '' );
440 + // The page the shell opens first. On the shell screen it is the
441 + // validated `target` query arg (else the session's focused window,
442 + // the default window, the Dashboard); on a solo boot it is the
443 + // request's own URL. Either way the frozen portal flags are gone
444 + // from it, so the derived window id matches what the dock would
445 + // produce for the same page — otherwise auto-opening the entry
446 + // window and clicking the same dock icon would create a duplicate.
447 + $boot_target = openstation_shell_boot_target();
448 + $current_page = $boot_target['url'];
449 + $from_portal = $boot_target['fromPortal'];
450 + $from_portal_intent = $boot_target['fromPortalIntent'];
404 451
405 - $from_portal = ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
406 - $from_portal_intent = ! empty( $_GET[ OPENSTATION_PORTAL_INTENT_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
452 + // On the shell screen `$title` and `$parent_file` describe the
453 + // screen ("OpenStation", no menu), not the page about to open. The
454 + // dock entry for that page is the identity the entry window folds
455 + // into, so its title and icon are the right first paint; the iframe
456 + // reports its own title once it lands either way.
457 + $current_title = wp_strip_all_tags( (string) $title );
458 + if ( openstation_is_shell_screen_request() ) {
459 + $boot_meta = openstation_shell_boot_target_meta( $current_page, $dock_items );
460 + $current_title = wp_strip_all_tags( $boot_meta['title'] );
461 + if ( '' !== $boot_meta['icon'] ) {
462 + $menu_icon = $boot_meta['icon'];
463 + }
464 + }
407 465
408 466 /**
409 467 * Filters the desktop shell configuration passed to JavaScript.
410 468 *
@@ -451,11 +509,15 @@
451 509 $config = apply_filters(
452 510 'openstation_shell_config',
453 511 array(
454 512 'currentPage' => esc_url( $current_page ),
455 - 'currentTitle' => wp_strip_all_tags( $title ),
513 + 'currentTitle' => $current_title,
456 514 'currentIcon' => sanitize_html_class( $menu_icon ),
457 - 'adminUrl' => esc_url( admin_url() ),
515 + // `self_admin_url()`: the base a window id is derived from,
516 + // and the URL the shell leaves for on exit. Both want the
517 + // admin the screen is in, which is the network one when the
518 + // network shell screen is what rendered.
519 + 'adminUrl' => esc_url( self_admin_url() ),
458 520 'homeUrl' => esc_url( home_url( '/' ) ),
459 521 // Decoded: the shell assigns this to `window.location`,
460 522 // where `&amp;` would make `_wpnonce` arrive as
461 523 // `amp;_wpnonce` and fail the nonce check.
@@ -468,8 +530,12 @@
468 530 // signature from this so the first off-allowlist menu change
469 531 // (vs. this boot state) is caught without a wasted probe. GH#325.
470 532 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
471 533 'nativeWindows' => $native_windows,
534 + // Handle-keyed script data the entries above reference —
535 + // one copy per bundle, not one per window. See
536 + // `openstation_collect_native_windows_payload()`.
537 + 'nativeWindowScriptData' => $native_window_script_data,
472 538 'serverWidgets' => $server_widgets,
473 539 'serverWallpapers' => $server_wallpapers,
474 540 'serverCommandScripts' => $server_command_scripts,
475 541 'serverCommands' => $server_commands,
@@ -512,9 +578,17 @@
512 578 'coreNotices' => openstation_get_core_notices(),
513 579 'pluginNotices' => openstation_get_plugin_notices(),
514 580 'defaultWallpaper' => openstation_get_default_wallpaper(),
515 581 'session' => openstation_get_session( get_current_user_id() ),
516 - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
582 + // The session route runs in the main site's blog context
583 + // whichever desktop posts to it, so the network screen's
584 + // URL says which session it is addressing — see
585 + // `openstation_rest_session_network()`.
586 + 'sessionUrl' => esc_url_raw(
587 + is_network_admin()
588 + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) )
589 + : rest_url( 'desktop-mode/v1/session' )
590 + ),
517 591 'restUrl' => esc_url_raw( rest_url() ),
518 592 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
519 593 'dropConfig' => $drop_config,
520 594 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
@@ -537,20 +611,29 @@
537 611 // stub `<script>`-injects this URL the first time the user
538 612 // opens the assistant. Picking `.js` vs `.min.js` here keeps
539 613 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
540 614 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
541 - // URL of the OS Settings panel lazy bundle. Injected by
542 - // the main bundle's `OsSettings.renderPanel()` stub on
543 - // the user's first Settings open. Holds every section
544 - // renderer + the `<os-*>` components only the panel
545 - // uses, so nothing about Settings ships in
546 - // `desktop.min.js` for users who never open it.
547 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
548 615 // URL of the shell-overlays lazy bundle. Pre-loaded by
549 616 // the main bundle after first paint so action-triggered
550 617 // overlays (toast, confirm dialog, context menus) feel
551 618 // instant the first time they fire.
552 619 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
620 + // The shell-bundle diet: features whose right moment is a
621 + // user gesture (or a presence signal) ride their own
622 + // bundles instead of the boot-critical `desktop[.min].js`.
623 + // Each sentinel in the shell loads its bundle at that
624 + // moment — see the entry file each bundle names.
625 + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ),
626 + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ),
627 + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ),
628 + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ),
629 + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ),
630 + // Presence hint for the notes sentinel: a desktop with no
631 + // notes skips the notes bundle AND the boot-time list
632 + // request. Two id-only existence probes at most.
633 + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' )
634 + ? openstation_notes_user_has_any()
635 + : false,
553 636 // URL of the full `<os-*>` component kit. The shell
554 637 // never loads this — its own bundles import the
555 638 // components they render. It exists for
556 639 // `wp.os.loadComponents()`, i.e. for plugin code that
@@ -584,12 +667,29 @@
584 667 // / `openNew()` call (both async); pre-loaded
585 668 // by the shell after first paint when no session is being
586 669 // restored and no `openCurrentPage` will fire.
587 670 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
671 + // URL of the lazy phone-layer bundle. Injected by the main
672 + // bundle only when the mode resolves to `mobile`, so a
673 + // desktop never fetches it. `mode` carries the preference
674 + // and breakpoints the first-paint head stamp already used,
675 + // plus the server's default tab-bar pins — see
676 + // `includes/mobile.php`.
677 + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ),
678 + 'mode' => openstation_mode_config( get_current_user_id() ),
588 679 // URL of the item-visibility-menu lazy bundle — the
589 680 // right-click "hide from dock / desktop" menu. Injected by
590 681 // the main bundle's loader shim on the first right-click.
591 682 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
683 + // URL of the workspace-wizard lazy bundle — the modal
684 + // behind "Edit this workspace…". Injected by the main
685 + // bundle's loader shim on first open.
686 + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ),
687 + // Server-side view of the workspace templates, so a plugin
688 + // can add or drop one from PHP. The client merges these
689 + // with its own built-ins by id — see
690 + // `src/workspaces/server-sync.ts`.
691 + 'workspacePresets' => openstation_workspace_presets(),
592 692 // URL of the release-card lazy bundle — the vinyl core-
593 693 // update announcement. Injected by `maybeShowUpdate()` only
594 694 // when a core update is actually pending.
595 695 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
@@ -621,41 +721,57 @@
621 721 // Site-wide games kill switch (Extended options). Exposed to
622 722 // every user — the shell skips the challenges Heartbeat
623 723 // channel when the framework is off.
624 724 'gamesEnabled' => openstation_games_enabled(),
625 - // Comments-window AI moderation toggle — surfaced at the
626 - // shell level so the OS Settings → Features tab can render
627 - // the toggle without depending on the Comments window
628 - // being registered for this user. URL is the same
629 - // endpoint the comments-window config exposes; state is
630 - // `null` for non-admins (the UI hides the row entirely).
631 - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
632 - // Non-null only for admins on a site where the Core AI stack is
633 - // present. Comment scoring routes through the AI Client (WP 7.0+),
634 - // so on older WordPress the whole row is hidden — same as the
635 - // assistant toggle — rather than shown disabled pointing at a
636 - // Settings → Connectors screen that doesn't exist there.
637 - 'commentsAi' => (
638 - current_user_can( 'manage_options' )
639 - && function_exists( 'openstation_ai_is_available' )
640 - && openstation_ai_is_available()
641 - )
642 - ? array(
643 - 'enabled' => function_exists( 'openstation_comments_ai_is_enabled' )
644 - ? openstation_comments_ai_is_enabled()
645 - : false,
646 - 'providerConfigured' => function_exists( 'openstation_comments_ai_provider_configured' )
647 - ? openstation_comments_ai_provider_configured()
648 - : false,
649 - )
650 - : null,
651 725 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
726 + // Null on single-site installs; its `networkAdmin` null
727 + // without `manage_network`, which is what keeps the dock
728 + // tile from registering.
729 + 'multisite' => openstation_multisite_payload(),
652 730 'portalUrl' => esc_url( openstation_portal_url() ),
653 731 'fromPortal' => $from_portal,
654 732 'fromPortalIntent' => $from_portal_intent,
733 + // One-shot like the boot target: a switch from another
734 + // site's overview lands in this one's.
735 + 'landInOverview' => openstation_shell_lands_in_overview(),
736 + 'arrivalDirection' => openstation_shell_arrival_direction(),
737 + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null,
655 738 'pwa' => array(
656 739 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
657 740 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ),
741 + // Extensionless retry target for hosts whose nginx 404s
742 + // virtual .js paths before WordPress runs (WordPress.com).
743 + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ),
744 + // The site's home path — the scope the registration
745 + // asks for, so a subdirectory network's sites each get
746 + // their own worker instead of fighting over the root.
747 + 'swScope' => openstation_pwa_sw_scope(),
748 + // The worker's per-user flags, computed HERE rather than
749 + // baked into the served `sw.js`.
750 + //
751 + // A service worker is origin-wide but these are per-user
752 + // preferences, so putting them in the script bytes made
753 + // the body differ between an anonymous and a logged-in
754 + // request — and any in-scope logged-out navigation then
755 + // installed a "new" worker, which at the time reloaded
756 + // the shell. The bytes are identical for everyone now;
757 + // the shell posts these to the worker at boot.
758 + //
759 + // Computed server-side, not read from the settings
760 + // snapshot client-side, because
761 + // `openstation_pwa_admin_asset_cache_enabled()` applies
762 + // the `openstation_pwa_admin_asset_cache` filter — an
763 + // operator's site-wide veto has to keep working.
764 + 'swConfig' => array(
765 + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(),
766 + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ),
767 + ),
768 + // The build this shell document belongs to. When a new
769 + // worker takes over mid-session the shell asks it for
770 + // the stamp it was served with and compares; only a
771 + // difference — the shell's own files changed on the
772 + // server — offers the user a reload. Never automatic.
773 + 'shellBuild' => openstation_shell_build_stamp(),
658 774 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
659 775 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
660 776 // Mirrors the manifest's `name` field — used by the
661 777 // install pill so the button reads "Install <site>"
@@ -684,13 +800,39 @@
684 800 // the first time its surface opens, via
685 801 // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
686 802 // Same resolved shape a native window's `styleUrl` /
687 803 // `styleInline` travels in.
804 + // Which of the `deferredStyles` entries a game needs.
805 + // `launchGame()` injects these before the window paints.
806 + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' )
807 + ? openstation_games_style_handles()
808 + : array(),
688 809 'deferredStyles' => openstation_build_deferred_styles(
689 - array(
690 - 'os-settings',
691 - 'desktop-mode-ai-assistant',
692 - 'desktop-mode-bug-report',
810 + array_merge(
811 + array(
812 + 'desktop-mode-ai-assistant',
813 + 'desktop-mode-bug-report',
814 + // The explorer's shared sheet. It rides the WP
815 + // Explorer APP as a companion style, but the
816 + // desktop FOLDER window paints its preview pane
817 + // with the same `os-my-wordpress__*` classes and
818 + // — being a native window opened straight from
819 + // JS — carries no companion styles of its own.
820 + // Without this, the pane rendered unstyled until
821 + // the explorer had been opened once in the
822 + // session.
823 + 'desktop-mode-my-wordpress',
824 + ),
825 + // The Games sheets. They also ride the hub window as
826 + // companion styles, but a game is reachable without
827 + // the hub — the challenge toast, solo mode, and
828 + // `wp.os.games.launch()` all land in `launchGame()`
829 + // with no hub window in the tab. Listing them here
830 + // costs a URL each in the boot config and no CSS
831 + // until `launchGame()` asks.
832 + function_exists( 'openstation_games_style_handles' )
833 + ? openstation_games_style_handles()
834 + : array()
693 835 )
694 836 ),
695 837 )
696 838 );
@@ -717,19 +859,26 @@
717 859 * deferred manifest (`openstation_build_command_palette_assets_payload()`)
718 860 * capture the chain instead, and the shell loads it on the first
719 861 * palette invocation.
720 862 *
721 - * Deliberately scoped: chromeless iframes and classic-mode requests
722 - * keep Core's default — inside an iframe the runtime powers the
723 - * command harvest the bridge streams to the parent, and a classic
724 - * page is Core's own UI where Core's palette is the right one.
863 + * Deliberately scoped: classic-mode requests keep Core's default,
864 + * because a classic page is Core's own UI where Core's palette is the
865 + * right one.
725 866 *
867 + * Windows are handled separately by
868 + * {@see openstation_chromeless_should_trim_command_palette()} in
869 + * `includes/render/chromeless-trim.php` — same idea, but it has to
870 + * drop the whole palette *family* rather than just unhook Core's
871 + * callback, and it exempts block-editor screens. Unhooking alone is
872 + * not enough there: a third-party palette extension that declares
873 + * `wp-commands` keeps the entire chain queued as its dependency.
874 + *
726 875 * Priority 0, ahead of Core's default 10, so the removal lands
727 876 * before the callback fires. On WP 6.9 (function exists, no default
728 877 * hook) the `remove_action()` is a harmless no-op.
729 878 */
730 879 function openstation_defer_core_command_palette() {
731 - if ( ! openstation_is_enabled() || openstation_is_chromeless_request() || openstation_is_classic_request() ) {
880 + if ( ! openstation_is_shell_request() ) {
732 881 return;
733 882 }
734 883 remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
735 884 }
@@ -779,14 +928,9 @@
779 928 * supply absolute URLs through the filter; in that case the consumer
780 929 * is responsible for the `crossorigin` semantics.
781 930 */
782 931 function openstation_print_preload_hints() {
783 - if (
784 - ! is_admin()
785 - || ! openstation_is_enabled()
786 - || openstation_is_chromeless_request()
787 - || openstation_is_classic_request()
788 - ) {
932 + if ( ! openstation_is_shell_request() ) {
789 933 return;
790 934 }
791 935
792 936 $suffix = openstation_asset_suffix();
@@ -829,8 +973,21 @@
829 973 'rel' => 'prefetch',
830 974 ),
831 975 );
832 976
977 + // The phone layer is needed at boot on a phone and never on a
978 + // desktop; the server cannot see the viewport, so the user agent
979 + // decides whether the hint is worth its bytes. A wrong guess costs
980 + // one low-priority fetch, never a wrong layout — the stamp and the
981 + // bundle loader read the real viewport.
982 + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) {
983 + $hints[] = array(
984 + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ),
985 + 'as' => 'script',
986 + 'rel' => 'prefetch',
987 + );
988 + }
989 +
833 990 /**
834 991 * Filters the list of resource preload hints emitted in `<head>`.
835 992 *
836 993 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -947,9 +1104,8 @@
947 1104 'os-openstation-layout',
948 1105 'desktop-mode-ai-assistant',
949 1106 'desktop-mode-bug-report',
950 1107 'os-window-overview',
951 - 'os-settings',
952 1108 )
953 1109 );
954 1110
955 1111 if ( ! in_array( $handle, (array) $deferred, true ) ) {