PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/render/assets.php +328 -86 1.1.2 → 1.1.12 View file →
@@ -73,26 +73,31 @@
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( 'os-notch' );
92 + wp_enqueue_style( 'os-workspaces' );
89 93 wp_enqueue_style( 'os-shortcuts' );
90 94 wp_enqueue_style( 'os-openstation-layout' );
91 - wp_enqueue_style( 'desktop-mode-ai-assistant' );
92 - wp_enqueue_style( 'desktop-mode-bug-report' );
93 95 wp_enqueue_style( 'os-files' );
94 96 wp_enqueue_style( 'os-notes' );
97 + // Unconditional like the layout sheet: a live crossing into the
98 + // phone band must not find the phone layer unstyled.
99 + wp_enqueue_style( 'os-mobile' );
95 100
96 101 // Solo mode — a single window freed into a native OS window by the
97 102 // desktop host. Same shell, everything but that one window hidden.
98 103 $solo_window = openstation_solo_window_id();
@@ -161,35 +166,35 @@
161 166 // WP normally only calls this on screens that opt in to the native
162 167 // palette; the shell needs it on every admin URL it might wrap.
163 168 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
164 169 // no-ops when the store is missing.
170 + // See `openstation_defer_core_command_palette()` below for why
171 + // Core's own boot-time enqueue is unhooked on shell pages.
172 + //
173 + // The Core command-palette runtime is NOT enqueued here any more.
174 + // Its dependency chain is the whole Gutenberg runtime (~800 KB
175 + // gzipped across forty-odd bundles), paid on every boot for a ⌘K
176 + // palette most sessions never open. It now ships as an ordered
177 + // manifest in the config blob (`commandPalette`, built by
178 + // `openstation_build_command_palette_assets_payload()`), and
179 + // `src/commands/palette-assets.ts` replays it the first time the
180 + // palette is invoked. The shell harvester keeps its idle-time
181 + // `install()` — a graceful no-op until the store exists — and
182 + // re-installs on `os-command-palette-ready`.
183 + $command_palette = openstation_build_command_palette_assets_payload();
184 +
165 185 if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
166 - // `wp_enqueue_command_palette_assets()` calls
167 - // `array_key_exists( $menu_slug, $submenu )` without guarding
168 - // the global, so an unset `$submenu` (test contexts, edge-case
169 - // admin requests where the menu wasn't built yet) blows up
170 - // with a TypeError. Initialize defensively before calling.
171 - global $menu, $submenu;
172 - // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- initializing an unset global to its documented empty shape, not replacing a built menu.
173 - if ( ! isset( $submenu ) || ! is_array( $submenu ) ) {
174 - $submenu = array();
175 - }
176 - if ( ! isset( $menu ) || ! is_array( $menu ) ) {
177 - $menu = array();
178 - }
179 - // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
180 - wp_enqueue_command_palette_assets();
181 -
182 186 // Expose the same menu-commands array WP serializes into
183 187 // `wp.coreCommands.initializeCommandPalette(...)` on a window
184 188 // slot the shell harvester can read. Built in PHP from `$menu`
185 - // / `$submenu` here (we already guarded that they're arrays
186 - // above), then injected as a `before` inline on our own bundle
187 - // — that runs synchronously before `desktop.min.js` boots the
188 - // shell harvester, so the lookup is guaranteed populated by
189 - // the time `src/commands/shell-harvester.ts` classifies any
190 - // command. Decoupled from WP's command-palette mount timing
191 - // (which fires from a core-registered hook we can't reorder).
189 + // / `$submenu`, then injected as a `before` inline on our own
190 + // bundle — that runs synchronously before `desktop.min.js`
191 + // boots the shell harvester, so the lookup is guaranteed
192 + // populated by the time `src/commands/shell-harvester.ts`
193 + // classifies any command. Decoupled from WP's command-palette
194 + // mount timing (which fires from a core-registered hook we
195 + // can't reorder) — and, since the palette bundles went lazy,
196 + // from whether they have loaded at all.
192 197 $menu_map = openstation_build_command_menu_map();
193 198 wp_add_inline_script(
194 199 'openstation',
195 200 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
@@ -197,9 +202,9 @@
197 202 );
198 203 }
199 204
200 205 // Pass configuration to JavaScript.
201 - global $title, $pagenow, $parent_file, $menu;
206 + global $title, $parent_file, $menu;
202 207
203 208 $menu_icon = 'dashicons-admin-generic';
204 209 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
205 210 foreach ( $menu as $item ) {
@@ -215,13 +220,32 @@
215 220 // plugin-contributed top-level routes. `openstation_dock_placement`
216 221 // is the per-item filter escape hatch for hiding. Shared with the
217 222 // REST menu endpoint so live refreshes (post plugin-activation)
218 223 // produce the same ordering as the boot payload.
219 - $menu_payload = openstation_build_menu_payload();
220 - $dock_items = $menu_payload['dockItems'];
221 - $native_windows = isset( $menu_payload['nativeWindows'] )
224 + $menu_payload = openstation_build_menu_payload();
225 + $dock_items = $menu_payload['dockItems'];
226 + $native_windows = isset( $menu_payload['nativeWindows'] )
222 227 ? $menu_payload['nativeWindows']
223 228 : array();
229 +
230 + // The BOOT page prints every registry window's template as a real
231 + // `<template>` tag (`openstation_render_native_window_templates()`,
232 + // admin_footer @ 20 — before footer scripts, so the tags are in
233 + // the DOM before the shell boots and `ensureTemplate()` adopts
234 + // them by id). The payload's `templateHtml` copy exists for the
235 + // MID-SESSION path — a bridge or probe payload delivering a
236 + // window whose plugin activated after the page rendered — so on
237 + // the boot config it is ~27 KB of the same markup twice. Strip it
238 + // here, and only here: the bridge and probe payloads keep theirs.
239 + foreach ( $native_windows as &$native_window_row ) {
240 + if ( is_array( $native_window_row ) ) {
241 + $native_window_row['templateHtml'] = '';
242 + }
243 + }
244 + unset( $native_window_row );
245 + $native_window_script_data = isset( $menu_payload['nativeWindowScriptData'] )
246 + ? $menu_payload['nativeWindowScriptData']
247 + : array();
224 248 $server_widgets = isset( $menu_payload['serverWidgets'] )
225 249 ? $menu_payload['serverWidgets']
226 250 : array();
227 251 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
@@ -288,9 +312,29 @@
288 312 // dedupes on an `activeId` that was never set.
289 313 $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
290 314 ? $menu_payload['serverDesktopThemes']
291 315 : array();
292 - $desktop_icons = isset( $menu_payload['desktopIcons'] )
316 +
317 + // Slim the theme library for BOOT: `cssText` and `tokens` are
318 + // each ~20 KB per theme, and neither is read at boot — the ACTIVE
319 + // theme's stylesheet is server-delivered (see
320 + // `openstation_enqueue_desktop_theme_style()`, whose stamp
321 + // `bootAlreadyApplied()` detects), and an inactive theme's CSS
322 + // only matters at the moment the user picks it in the Preferences
323 + // picker — which fetches the full entries from
324 + // `GET desktop-mode/v1/desktop-themes` (`ensureFullDesktopThemes()`
325 + // client-side). `cssDeferred` marks the gap so the shell can tell
326 + // a slimmed entry from a theme that genuinely ships no CSS.
327 + // Bridge and probe payloads keep full entries.
328 + foreach ( $server_desktop_themes as &$desktop_theme_row ) {
329 + if ( is_array( $desktop_theme_row ) ) {
330 + $desktop_theme_row['cssText'] = '';
331 + $desktop_theme_row['tokens'] = new stdClass();
332 + $desktop_theme_row['cssDeferred'] = true;
333 + }
334 + }
335 + unset( $desktop_theme_row );
336 + $desktop_icons = isset( $menu_payload['desktopIcons'] )
293 337 ? $menu_payload['desktopIcons']
294 338 : array();
295 339
296 340 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -301,8 +345,16 @@
301 345 : array();
302 346 $server_file_openers = function_exists( 'openstation_build_file_openers_payload' )
303 347 ? openstation_build_file_openers_payload()
304 348 : array();
349 + // Entries in the menu payload carry dependency handles; their
350 + // payloads ride once in `scriptDepPayloads` (GH#892). The file
351 + // lists stay whole: no sync module reads them on the client, and
352 + // compacting them here was the only write to the map after the
353 + // menu payload froze its own, so a refresh could not match it.
354 + $script_dep_payloads = isset( $menu_payload['scriptDepPayloads'] )
355 + ? (array) $menu_payload['scriptDepPayloads']
356 + : array();
305 357 $user_file_associations = function_exists( 'openstation_get_user_file_associations' )
306 358 ? openstation_get_user_file_associations( get_current_user_id() )
307 359 : array();
308 360 $server_wallpaper_menu_items = function_exists( 'openstation_build_wallpaper_menu_items' )
@@ -391,18 +443,33 @@
391 443 OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
392 444 );
393 445 };
394 446
395 - // Build the current page URL from $pagenow + $_GET. Strip the portal
396 - // markers so the derived window ID matches what the dock would produce
397 - // for the same page — otherwise auto-opening the entry window and
398 - // clicking the same dock icon would create a duplicate.
399 - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
400 - unset( $current_query[ OPENSTATION_PORTAL_FLAG ], $current_query[ OPENSTATION_PORTAL_INTENT_FLAG ] );
401 - $current_page = admin_url( $pagenow ) . ( ! empty( $current_query ) ? '?' . http_build_query( $current_query ) : '' );
447 + // The page the shell opens first. On the shell screen it is the
448 + // validated `target` query arg (else the session's focused window,
449 + // the default window, the Dashboard); on a solo boot it is the
450 + // request's own URL. Either way the frozen portal flags are gone
451 + // from it, so the derived window id matches what the dock would
452 + // produce for the same page — otherwise auto-opening the entry
453 + // window and clicking the same dock icon would create a duplicate.
454 + $boot_target = openstation_shell_boot_target();
455 + $current_page = $boot_target['url'];
456 + $from_portal = $boot_target['fromPortal'];
457 + $from_portal_intent = $boot_target['fromPortalIntent'];
402 458
403 - $from_portal = ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
404 - $from_portal_intent = ! empty( $_GET[ OPENSTATION_PORTAL_INTENT_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
459 + // On the shell screen `$title` and `$parent_file` describe the
460 + // screen ("OpenStation", no menu), not the page about to open. The
461 + // dock entry for that page is the identity the entry window folds
462 + // into, so its title and icon are the right first paint; the iframe
463 + // reports its own title once it lands either way.
464 + $current_title = wp_strip_all_tags( (string) $title );
465 + if ( openstation_is_shell_screen_request() ) {
466 + $boot_meta = openstation_shell_boot_target_meta( $current_page, $dock_items );
467 + $current_title = wp_strip_all_tags( $boot_meta['title'] );
468 + if ( '' !== $boot_meta['icon'] ) {
469 + $menu_icon = $boot_meta['icon'];
470 + }
471 + }
405 472
406 473 /**
407 474 * Filters the desktop shell configuration passed to JavaScript.
408 475 *
@@ -442,9 +509,14 @@
442 509 * @type bool $fromPortal Whether the shell was reached via the portal.
443 510 * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/openstation/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for.
444 511 * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`).
445 512 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
513 + * @type bool $shellTour Whether this site offers the first-boot shell tour (`openstation_show_shell_tour`). Whether this user already had it is `seenIntros` containing `shell-tour`.
514 + * @type string $shellTourBundleUrl URL of the lazy shell-tour bundle, injected on first use.
515 + * @type array $firstRun `{ installedAt, firstEnabledAt, enabledAt }`, epoch seconds, 0 when unknown — the first-run stamps, read-only.
446 516 * @type bool $rebrandNotice Whether to offer this user the one-off announcement explaining the rename from Desktop Mode to OpenStation. True only when migration 5 flagged this user as a Desktop Mode user from before the rename AND they haven't dismissed the `openstation-rebrand` intro. Only ever present in the shell config, so the announcement never reaches the classic admin.
517 + * @type array|null $usageFeedback What the one-time usage feedback prompt needs (`restUrl`), or `null` when this user is not owed it: the feature is off, they have had OpenStation on for fewer than seven days by the `openstation_enabled_at` stamp, or they already answered or dismissed the `usage-feedback` intro. Carries no user data.
518 + * @type string $usageFeedbackBundleUrl URL of the lazy `usage-feedback` bundle, the form the prompt opens.
447 519 * }
448 520 */
449 521 $config = apply_filters(
450 522 'openstation_shell_config',
@@ -449,11 +521,15 @@
449 521 $config = apply_filters(
450 522 'openstation_shell_config',
451 523 array(
452 524 'currentPage' => esc_url( $current_page ),
453 - 'currentTitle' => wp_strip_all_tags( $title ),
525 + 'currentTitle' => $current_title,
454 526 'currentIcon' => sanitize_html_class( $menu_icon ),
455 - 'adminUrl' => esc_url( admin_url() ),
527 + // `self_admin_url()`: the base a window id is derived from,
528 + // and the URL the shell leaves for on exit. Both want the
529 + // admin the screen is in, which is the network one when the
530 + // network shell screen is what rendered.
531 + 'adminUrl' => esc_url( self_admin_url() ),
456 532 'homeUrl' => esc_url( home_url( '/' ) ),
457 533 // Decoded: the shell assigns this to `window.location`,
458 534 // where `&amp;` would make `_wpnonce` arrive as
459 535 // `amp;_wpnonce` and fail the nonce check.
@@ -466,8 +542,12 @@
466 542 // signature from this so the first off-allowlist menu change
467 543 // (vs. this boot state) is caught without a wasted probe. GH#325.
468 544 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
469 545 'nativeWindows' => $native_windows,
546 + // Handle-keyed script data the entries above reference —
547 + // one copy per bundle, not one per window. See
548 + // `openstation_collect_native_windows_payload()`.
549 + 'nativeWindowScriptData' => $native_window_script_data,
470 550 'serverWidgets' => $server_widgets,
471 551 'serverWallpapers' => $server_wallpapers,
472 552 'serverCommandScripts' => $server_command_scripts,
473 553 'serverCommands' => $server_commands,
@@ -493,8 +573,11 @@
493 573 'serverDesktopThemes' => $server_desktop_themes,
494 574 'desktopIcons' => $desktop_icons,
495 575 'serverFileTypes' => $server_file_types,
496 576 'serverFileOpeners' => $server_file_openers,
577 + // Handle => dependency payload for every `scriptDeps` list in
578 + // this config; see `openstation_compact_script_deps()`.
579 + 'scriptDepPayloads' => (object) $script_dep_payloads,
497 580 'userFileAssociations' => $user_file_associations,
498 581 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
499 582 // Pinned-notes REST base (`includes/notes/rest.php`). The
500 583 // notes layer boots only when this is present.
@@ -510,9 +593,17 @@
510 593 'coreNotices' => openstation_get_core_notices(),
511 594 'pluginNotices' => openstation_get_plugin_notices(),
512 595 'defaultWallpaper' => openstation_get_default_wallpaper(),
513 596 'session' => openstation_get_session( get_current_user_id() ),
514 - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
597 + // The session route runs in the main site's blog context
598 + // whichever desktop posts to it, so the network screen's
599 + // URL says which session it is addressing — see
600 + // `openstation_rest_session_network()`.
601 + 'sessionUrl' => esc_url_raw(
602 + is_network_admin()
603 + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) )
604 + : rest_url( 'desktop-mode/v1/session' )
605 + ),
515 606 'restUrl' => esc_url_raw( rest_url() ),
516 607 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
517 608 'dropConfig' => $drop_config,
518 609 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
@@ -535,20 +626,29 @@
535 626 // stub `<script>`-injects this URL the first time the user
536 627 // opens the assistant. Picking `.js` vs `.min.js` here keeps
537 628 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
538 629 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
539 - // URL of the OS Settings panel lazy bundle. Injected by
540 - // the main bundle's `OsSettings.renderPanel()` stub on
541 - // the user's first Settings open. Holds every section
542 - // renderer + the `<os-*>` components only the panel
543 - // uses, so nothing about Settings ships in
544 - // `desktop.min.js` for users who never open it.
545 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
546 630 // URL of the shell-overlays lazy bundle. Pre-loaded by
547 631 // the main bundle after first paint so action-triggered
548 632 // overlays (toast, confirm dialog, context menus) feel
549 633 // instant the first time they fire.
550 634 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
635 + // The shell-bundle diet: features whose right moment is a
636 + // user gesture (or a presence signal) ride their own
637 + // bundles instead of the boot-critical `desktop[.min].js`.
638 + // Each sentinel in the shell loads its bundle at that
639 + // moment — see the entry file each bundle names.
640 + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ),
641 + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ),
642 + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ),
643 + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ),
644 + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ),
645 + // Presence hint for the notes sentinel: a desktop with no
646 + // notes skips the notes bundle AND the boot-time list
647 + // request. Two id-only existence probes at most.
648 + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' )
649 + ? openstation_notes_user_has_any()
650 + : false,
551 651 // URL of the full `<os-*>` component kit. The shell
552 652 // never loads this — its own bundles import the
553 653 // components they render. It exists for
554 654 // `wp.os.loadComponents()`, i.e. for plugin code that
@@ -582,12 +682,29 @@
582 682 // / `openNew()` call (both async); pre-loaded
583 683 // by the shell after first paint when no session is being
584 684 // restored and no `openCurrentPage` will fire.
585 685 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
686 + // URL of the lazy phone-layer bundle. Injected by the main
687 + // bundle only when the mode resolves to `mobile`, so a
688 + // desktop never fetches it. `mode` carries the preference
689 + // and breakpoints the first-paint head stamp already used,
690 + // plus the server's default tab-bar pins — see
691 + // `includes/mobile.php`.
692 + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ),
693 + 'mode' => openstation_mode_config( get_current_user_id() ),
586 694 // URL of the item-visibility-menu lazy bundle — the
587 695 // right-click "hide from dock / desktop" menu. Injected by
588 696 // the main bundle's loader shim on the first right-click.
589 697 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
698 + // URL of the workspace-wizard lazy bundle — the modal
699 + // behind "Edit this workspace…". Injected by the main
700 + // bundle's loader shim on first open.
701 + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ),
702 + // Server-side view of the workspace templates, so a plugin
703 + // can add or drop one from PHP. The client merges these
704 + // with its own built-ins by id — see
705 + // `src/workspaces/server-sync.ts`.
706 + 'workspacePresets' => openstation_workspace_presets(),
590 707 // URL of the release-card lazy bundle — the vinyl core-
591 708 // update announcement. Injected by `maybeShowUpdate()` only
592 709 // when a core update is actually pending.
593 710 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
@@ -604,8 +721,26 @@
604 721 // announcement yet. Same value that gated `os-announce`
605 722 // above; the dialog cannot paint without that stylesheet, so
606 723 // the two must not diverge.
607 724 'rebrandNotice' => $show_rebrand_notice,
725 + // The first-boot shell tour: whether this site offers it
726 + // (the `openstation_show_shell_tour` filter), and the lazy
727 + // bundle that runs it. Whether THIS user already had it is
728 + // `seenIntros` containing `shell-tour`; the shell reads that
729 + // itself so a reset can replay the tour without a new boot.
730 + 'shellTour' => openstation_should_offer_shell_tour( get_current_user_id() ),
731 + 'shellTourBundleUrl' => $lazy_bundle_url( 'shell-tour' ),
732 + // The first-run stamps, read-only, epoch seconds (0 when
733 + // unknown): when the plugin was installed, when anyone first
734 + // enabled it, and when this user did. See
735 + // `includes/first-run/stamps.php`.
736 + 'firstRun' => openstation_first_run_config( get_current_user_id() ),
737 + // Null for everyone but a user who has had OpenStation on
738 + // long enough and has not answered yet; the gate lives in
739 + // `includes/feedback/usage.php`. The form is a lazy bundle
740 + // fetched only when the user says yes to the prompt.
741 + 'usageFeedback' => function_exists( 'openstation_usage_feedback_config' ) ? openstation_usage_feedback_config() : null,
742 + 'usageFeedbackBundleUrl' => $lazy_bundle_url( 'usage-feedback' ),
608 743 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
609 744 // AI assistant availability + per-user toggle. Drives whether the
610 745 // Cmd+K palette and admin-bar icon appear, and the setup placeholder.
611 746 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' )
@@ -619,41 +754,57 @@
619 754 // Site-wide games kill switch (Extended options). Exposed to
620 755 // every user — the shell skips the challenges Heartbeat
621 756 // channel when the framework is off.
622 757 'gamesEnabled' => openstation_games_enabled(),
623 - // Comments-window AI moderation toggle — surfaced at the
624 - // shell level so the OS Settings → Features tab can render
625 - // the toggle without depending on the Comments window
626 - // being registered for this user. URL is the same
627 - // endpoint the comments-window config exposes; state is
628 - // `null` for non-admins (the UI hides the row entirely).
629 - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
630 - // Non-null only for admins on a site where the Core AI stack is
631 - // present. Comment scoring routes through the AI Client (WP 7.0+),
632 - // so on older WordPress the whole row is hidden — same as the
633 - // assistant toggle — rather than shown disabled pointing at a
634 - // Settings → Connectors screen that doesn't exist there.
635 - 'commentsAi' => (
636 - current_user_can( 'manage_options' )
637 - && function_exists( 'openstation_ai_is_available' )
638 - && openstation_ai_is_available()
639 - )
640 - ? array(
641 - 'enabled' => function_exists( 'openstation_comments_ai_is_enabled' )
642 - ? openstation_comments_ai_is_enabled()
643 - : false,
644 - 'providerConfigured' => function_exists( 'openstation_comments_ai_provider_configured' )
645 - ? openstation_comments_ai_provider_configured()
646 - : false,
647 - )
648 - : null,
649 758 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
759 + // Null on single-site installs; its `networkAdmin` null
760 + // without `manage_network`, which is what keeps the dock
761 + // tile from registering.
762 + 'multisite' => openstation_multisite_payload(),
650 763 'portalUrl' => esc_url( openstation_portal_url() ),
651 764 'fromPortal' => $from_portal,
652 765 'fromPortalIntent' => $from_portal_intent,
766 + // One-shot like the boot target: a switch from another
767 + // site's overview lands in this one's.
768 + 'landInOverview' => openstation_shell_lands_in_overview(),
769 + 'arrivalDirection' => openstation_shell_arrival_direction(),
770 + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null,
653 771 'pwa' => array(
654 772 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
655 773 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ),
774 + // Extensionless retry target for hosts whose nginx 404s
775 + // virtual .js paths before WordPress runs (WordPress.com).
776 + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ),
777 + // The site's home path — the scope the registration
778 + // asks for, so a subdirectory network's sites each get
779 + // their own worker instead of fighting over the root.
780 + 'swScope' => openstation_pwa_sw_scope(),
781 + // The worker's per-user flags, computed HERE rather than
782 + // baked into the served `sw.js`.
783 + //
784 + // A service worker is origin-wide but these are per-user
785 + // preferences, so putting them in the script bytes made
786 + // the body differ between an anonymous and a logged-in
787 + // request — and any in-scope logged-out navigation then
788 + // installed a "new" worker, which at the time reloaded
789 + // the shell. The bytes are identical for everyone now;
790 + // the shell posts these to the worker at boot.
791 + //
792 + // Computed server-side, not read from the settings
793 + // snapshot client-side, because
794 + // `openstation_pwa_admin_asset_cache_enabled()` applies
795 + // the `openstation_pwa_admin_asset_cache` filter — an
796 + // operator's site-wide veto has to keep working.
797 + 'swConfig' => array(
798 + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(),
799 + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ),
800 + ),
801 + // The build this shell document belongs to. When a new
802 + // worker takes over mid-session the shell asks it for
803 + // the stamp it was served with and compares; only a
804 + // difference — the shell's own files changed on the
805 + // server — offers the user a reload. Never automatic.
806 + 'shellBuild' => openstation_shell_build_stamp(),
656 807 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
657 808 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
658 809 // Mirrors the manifest's `name` field — used by the
659 810 // install pill so the button reads "Install <site>"
@@ -669,8 +820,54 @@
669 820 // `src/pwa/sw-register.ts`). Default `false` preserves
670 821 // the polite behaviour where we yield to existing PWAs.
671 822 'forceReplaceSw' => openstation_pwa_force_replace_sw(),
672 823 ),
824 + // Ordered Core command-palette asset manifest, replayed on
825 + // first palette invocation. `null` on pre-6.9 sites.
826 + 'commandPalette' => $command_palette,
827 + // Stylesheets for shell surfaces that render on demand —
828 + // the Preferences panel, the AI assistant, the bug-report
829 + // window. None of them is a server-registered native
830 + // window (they are built client-side by the shell
831 + // bundle), so the `styles` companion mechanism can't
832 + // carry their CSS; instead the shell injects each sheet
833 + // the first time its surface opens, via
834 + // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
835 + // Same resolved shape a native window's `styleUrl` /
836 + // `styleInline` travels in.
837 + // Which of the `deferredStyles` entries a game needs.
838 + // `launchGame()` injects these before the window paints.
839 + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' )
840 + ? openstation_games_style_handles()
841 + : array(),
842 + 'deferredStyles' => openstation_build_deferred_styles(
843 + array_merge(
844 + array(
845 + 'desktop-mode-ai-assistant',
846 + 'desktop-mode-bug-report',
847 + // The explorer's shared sheet. It rides the WP
848 + // Explorer APP as a companion style, but the
849 + // desktop FOLDER window paints its preview pane
850 + // with the same `os-my-wordpress__*` classes and
851 + // — being a native window opened straight from
852 + // JS — carries no companion styles of its own.
853 + // Without this, the pane rendered unstyled until
854 + // the explorer had been opened once in the
855 + // session.
856 + 'desktop-mode-my-wordpress',
857 + ),
858 + // The Games sheets. They also ride the hub window as
859 + // companion styles, but a game is reachable without
860 + // the hub — the challenge toast, solo mode, and
861 + // `wp.os.games.launch()` all land in `launchGame()`
862 + // with no hub window in the tab. Listing them here
863 + // costs a URL each in the boot config and no CSS
864 + // until `launchGame()` asks.
865 + function_exists( 'openstation_games_style_handles' )
866 + ? openstation_games_style_handles()
867 + : array()
868 + )
869 + ),
673 870 )
674 871 );
675 872
676 873 wp_localize_script( 'openstation', 'openStationConfig', $config );
@@ -682,8 +879,46 @@
682 879 }
683 880 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' );
684 881
685 882 /**
883 + * Keep Core's boot-time command-palette enqueue off shell pages.
884 + *
885 + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on
886 + * `admin_enqueue_scripts` by default, which puts the palette's whole
887 + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on
888 + * every admin page. On a SHELL page that is pure dead weight: the
889 + * shell suppresses Core's palette unconditionally (the ⌘K keystroke
890 + * and the admin-bar icon both route to the shell's own palette), so
891 + * the runtime it powers can never be shown. Unhooking here lets the
892 + * deferred manifest (`openstation_build_command_palette_assets_payload()`)
893 + * capture the chain instead, and the shell loads it on the first
894 + * palette invocation.
895 + *
896 + * Deliberately scoped: classic-mode requests keep Core's default,
897 + * because a classic page is Core's own UI where Core's palette is the
898 + * right one.
899 + *
900 + * Windows are handled separately by
901 + * {@see openstation_chromeless_should_trim_command_palette()} in
902 + * `includes/render/chromeless-trim.php` — same idea, but it has to
903 + * drop the whole palette *family* rather than just unhook Core's
904 + * callback, and it exempts block-editor screens. Unhooking alone is
905 + * not enough there: a third-party palette extension that declares
906 + * `wp-commands` keeps the entire chain queued as its dependency.
907 + *
908 + * Priority 0, ahead of Core's default 10, so the removal lands
909 + * before the callback fires. On WP 6.9 (function exists, no default
910 + * hook) the `remove_action()` is a harmless no-op.
911 + */
912 +function openstation_defer_core_command_palette() {
913 + if ( ! openstation_is_shell_request() ) {
914 + return;
915 + }
916 + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
917 +}
918 +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 );
919 +
920 +/**
686 921 * Emits `<link rel="preload">` hints for the shell's critical-path
687 922 * assets so the browser starts fetching them as soon as it parses
688 923 * the document `<head>`.
689 924 *
@@ -726,14 +961,9 @@
726 961 * supply absolute URLs through the filter; in that case the consumer
727 962 * is responsible for the `crossorigin` semantics.
728 963 */
729 964 function openstation_print_preload_hints() {
730 - if (
731 - ! is_admin()
732 - || ! openstation_is_enabled()
733 - || openstation_is_chromeless_request()
734 - || openstation_is_classic_request()
735 - ) {
965 + if ( ! openstation_is_shell_request() ) {
736 966 return;
737 967 }
738 968
739 969 $suffix = openstation_asset_suffix();
@@ -776,8 +1006,21 @@
776 1006 'rel' => 'prefetch',
777 1007 ),
778 1008 );
779 1009
1010 + // The phone layer is needed at boot on a phone and never on a
1011 + // desktop; the server cannot see the viewport, so the user agent
1012 + // decides whether the hint is worth its bytes. A wrong guess costs
1013 + // one low-priority fetch, never a wrong layout — the stamp and the
1014 + // bundle loader read the real viewport.
1015 + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) {
1016 + $hints[] = array(
1017 + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ),
1018 + 'as' => 'script',
1019 + 'rel' => 'prefetch',
1020 + );
1021 + }
1022 +
780 1023 /**
781 1024 * Filters the list of resource preload hints emitted in `<head>`.
782 1025 *
783 1026 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -894,9 +1137,8 @@
894 1137 'os-openstation-layout',
895 1138 'desktop-mode-ai-assistant',
896 1139 'desktop-mode-bug-report',
897 1140 'os-window-overview',
898 - 'os-settings',
899 1141 )
900 1142 );
901 1143
902 1144 if ( ! in_array( $handle, (array) $deferred, true ) ) {