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 +327 -102 1.0.11.1.10 View file →
@@ -73,23 +73,32 @@
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
93 102 // Solo mode — a single window freed into a native OS window by the
94 103 // desktop host. Same shell, everything but that one window hidden.
95 104 $solo_window = openstation_solo_window_id();
@@ -158,35 +167,35 @@
158 167 // WP normally only calls this on screens that opt in to the native
159 168 // palette; the shell needs it on every admin URL it might wrap.
160 169 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
161 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 +
162 186 if ( function_exists( 'wp_enqueue_command_palette_assets' ) ) {
163 - // `wp_enqueue_command_palette_assets()` calls
164 - // `array_key_exists( $menu_slug, $submenu )` without guarding
165 - // the global, so an unset `$submenu` (test contexts, edge-case
166 - // admin requests where the menu wasn't built yet) blows up
167 - // with a TypeError. Initialize defensively before calling.
168 - global $menu, $submenu;
169 - // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- initializing an unset global to its documented empty shape, not replacing a built menu.
170 - if ( ! isset( $submenu ) || ! is_array( $submenu ) ) {
171 - $submenu = array();
172 - }
173 - if ( ! isset( $menu ) || ! is_array( $menu ) ) {
174 - $menu = array();
175 - }
176 - // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
177 - wp_enqueue_command_palette_assets();
178 -
179 187 // Expose the same menu-commands array WP serializes into
180 188 // `wp.coreCommands.initializeCommandPalette(...)` on a window
181 189 // slot the shell harvester can read. Built in PHP from `$menu`
182 - // / `$submenu` here (we already guarded that they're arrays
183 - // above), then injected as a `before` inline on our own bundle
184 - // — that runs synchronously before `desktop.min.js` boots the
185 - // shell harvester, so the lookup is guaranteed populated by
186 - // the time `src/commands/shell-harvester.ts` classifies any
187 - // command. Decoupled from WP's command-palette mount timing
188 - // (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.
189 198 $menu_map = openstation_build_command_menu_map();
190 199 wp_add_inline_script(
191 200 'openstation',
192 201 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
@@ -194,9 +203,9 @@
194 203 );
195 204 }
196 205
197 206 // Pass configuration to JavaScript.
198 - global $title, $pagenow, $parent_file, $menu;
207 + global $title, $parent_file, $menu;
199 208
200 209 $menu_icon = 'dashicons-admin-generic';
201 210 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
202 211 foreach ( $menu as $item ) {
@@ -212,13 +221,32 @@
212 221 // plugin-contributed top-level routes. `openstation_dock_placement`
213 222 // is the per-item filter escape hatch for hiding. Shared with the
214 223 // REST menu endpoint so live refreshes (post plugin-activation)
215 224 // produce the same ordering as the boot payload.
216 - $menu_payload = openstation_build_menu_payload();
217 - $dock_items = $menu_payload['dockItems'];
218 - $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'] )
219 228 ? $menu_payload['nativeWindows']
220 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();
221 249 $server_widgets = isset( $menu_payload['serverWidgets'] )
222 250 ? $menu_payload['serverWidgets']
223 251 : array();
224 252 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
@@ -285,9 +313,29 @@
285 313 // dedupes on an `activeId` that was never set.
286 314 $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
287 315 ? $menu_payload['serverDesktopThemes']
288 316 : array();
289 - $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'] )
290 338 ? $menu_payload['desktopIcons']
291 339 : array();
292 340
293 341 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -364,9 +412,9 @@
364 412 'maxSize' => $drop_max_size,
365 413 );
366 414
367 415 // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant,
368 - // About-scene, OS Settings panel, shell-overlays, window-system)
416 + // OS Settings panel, shell-overlays, window-system)
369 417 // is `<script>`-injected by the main bundle on demand — they don't
370 418 // go through `wp_register_script`, so they don't pick up WordPress's
371 419 // usual `?ver=<filemtime>` cache-buster. Without one, the browser
372 420 // happily serves a stale cached copy across plugin updates that
@@ -388,18 +436,33 @@
388 436 OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
389 437 );
390 438 };
391 439
392 - // Build the current page URL from $pagenow + $_GET. Strip the portal
393 - // markers so the derived window ID matches what the dock would produce
394 - // for the same page — otherwise auto-opening the entry window and
395 - // clicking the same dock icon would create a duplicate.
396 - $current_query = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
397 - unset( $current_query[ OPENSTATION_PORTAL_FLAG ], $current_query[ OPENSTATION_PORTAL_INTENT_FLAG ] );
398 - $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'];
399 451
400 - $from_portal = ! empty( $_GET[ OPENSTATION_PORTAL_FLAG ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
401 - $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 + }
402 465
403 466 /**
404 467 * Filters the desktop shell configuration passed to JavaScript.
405 468 *
@@ -431,17 +494,17 @@
431 494 * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference.
432 495 * @type bool $canUpload Whether the user holds the `upload_files` capability.
433 496 * @type string $pluginUrl Plugin base URL (no trailing slash). Used by the shell to locate vendor assets and by plugins to build asset URLs.
434 497 * @type string $pluginVersion Plugin semver string. Surfaced in the OS Settings → About tab; plugins can read it to gate features by version.
498 + * @type string $aboutFeedUrl Authenticated admin-AJAX URL that returns the cached OpenStation journal feed for the About tab.
435 499 * @type string $restNonce Nonce for the session REST endpoint.
436 500 * @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.
437 501 * @type string $portalUrl Canonical `/openstation/` URL.
438 502 * @type bool $fromPortal Whether the shell was reached via the portal.
439 503 * @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.
440 - * @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.
504 + * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`).
441 505 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
442 506 * @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.
443 - * @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()`.
444 507 * }
445 508 */
446 509 $config = apply_filters(
447 510 'openstation_shell_config',
@@ -446,11 +509,22 @@
446 509 $config = apply_filters(
447 510 'openstation_shell_config',
448 511 array(
449 512 'currentPage' => esc_url( $current_page ),
450 - 'currentTitle' => wp_strip_all_tags( $title ),
513 + 'currentTitle' => $current_title,
451 514 'currentIcon' => sanitize_html_class( $menu_icon ),
452 - '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() ),
520 + 'homeUrl' => esc_url( home_url( '/' ) ),
521 + // Decoded: the shell assigns this to `window.location`,
522 + // where `&amp;` would make `_wpnonce` arrive as
523 + // `amp;_wpnonce` and fail the nonce check.
524 + 'logoutUrl' => esc_url_raw(
525 + html_entity_decode( wp_logout_url(), ENT_QUOTES, 'UTF-8' )
526 + ),
453 527 'colorScheme' => sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' ),
454 528 'dockItems' => $dock_items,
455 529 // Baseline menu fingerprint. The shell seeds its last-known
456 530 // signature from this so the first off-allowlist menu change
@@ -456,8 +530,12 @@
456 530 // signature from this so the first off-allowlist menu change
457 531 // (vs. this boot state) is caught without a wasted probe. GH#325.
458 532 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
459 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,
460 538 'serverWidgets' => $server_widgets,
461 539 'serverWallpapers' => $server_wallpapers,
462 540 'serverCommandScripts' => $server_command_scripts,
463 541 'serverCommands' => $server_commands,
@@ -500,9 +578,17 @@
500 578 'coreNotices' => openstation_get_core_notices(),
501 579 'pluginNotices' => openstation_get_plugin_notices(),
502 580 'defaultWallpaper' => openstation_get_default_wallpaper(),
503 581 'session' => openstation_get_session( get_current_user_id() ),
504 - '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 + ),
505 591 'restUrl' => esc_url_raw( rest_url() ),
506 592 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
507 593 'dropConfig' => $drop_config,
508 594 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
@@ -509,8 +595,17 @@
509 595 'defaultWindow' => openstation_get_default_window( get_current_user_id() ),
510 596 'canUpload' => current_user_can( 'upload_files' ),
511 597 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ),
512 598 'pluginVersion' => OPENSTATION_VERSION,
599 + 'aboutFeedUrl' => esc_url_raw(
600 + add_query_arg(
601 + array(
602 + 'action' => 'openstation_about_feed',
603 + 'nonce' => wp_create_nonce( 'openstation_about_feed' ),
604 + ),
605 + admin_url( 'admin-ajax.php' )
606 + )
607 + ),
513 608 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
514 609 // URL of the AI Assistant lazy bundle. The main bundle
515 610 // ships a stub matching the public `wp.os.ai` API; the
516 611 // stub `<script>`-injects this URL the first time the user
@@ -516,25 +611,39 @@
516 611 // stub `<script>`-injects this URL the first time the user
517 612 // opens the assistant. Picking `.js` vs `.min.js` here keeps
518 613 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
519 614 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
520 - // URL of the About-scene lazy bundle. The OS Settings →
521 - // About tab loads this on first mount; ~25 kB PixiJS
522 - // particle scene that would otherwise ship in the main
523 - // bundle for every shell load.
524 - 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ),
525 - // URL of the OS Settings panel lazy bundle. Injected by
526 - // the main bundle's `OsSettings.renderPanel()` stub on
527 - // the user's first Settings open. Holds every section
528 - // renderer + the `<os-*>` components only the panel
529 - // uses, so nothing about Settings ships in
530 - // `desktop.min.js` for users who never open it.
531 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
532 615 // URL of the shell-overlays lazy bundle. Pre-loaded by
533 616 // the main bundle after first paint so action-triggered
534 617 // overlays (toast, confirm dialog, context menus) feel
535 618 // instant the first time they fire.
536 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,
636 + // URL of the full `<os-*>` component kit. The shell
637 + // never loads this — its own bundles import the
638 + // components they render. It exists for
639 + // `wp.os.loadComponents()`, i.e. for plugin code that
640 + // CANNOT import: a plugin shipped as a zip has no path
641 + // to this repo at build time, so before this URL its
642 + // only routes to a `<os-switch>` were to bundle a second
643 + // copy or hand-roll one. Shipping the URL costs one
644 + // string and keeps the SCRIPT_DEBUG choice server-side.
645 + 'componentsBundleUrl' => $lazy_bundle_url( 'os-components' ),
537 646 // Mio — the desk companion. `mio` carries the
538 647 // appearance + physics (see `openstation_mio_config()`);
539 648 // `mioBundleUrl` is the lazy PixiJS bundle the shell
540 649 // controller injects the first time a user switches the
@@ -558,12 +667,29 @@
558 667 // / `openNew()` call (both async); pre-loaded
559 668 // by the shell after first paint when no session is being
560 669 // restored and no `openCurrentPage` will fire.
561 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() ),
562 679 // URL of the item-visibility-menu lazy bundle — the
563 680 // right-click "hide from dock / desktop" menu. Injected by
564 681 // the main bundle's loader shim on the first right-click.
565 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(),
566 692 // URL of the release-card lazy bundle — the vinyl core-
567 693 // update announcement. Injected by `maybeShowUpdate()` only
568 694 // when a core update is actually pending.
569 695 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
@@ -580,18 +706,9 @@
580 706 // announcement yet. Same value that gated `os-announce`
581 707 // above; the dialog cannot paint without that stylesheet, so
582 708 // the two must not diverge.
583 709 'rebrandNotice' => $show_rebrand_notice,
584 - // Sticky notes ride on Gutenberg's Guidelines experiment
585 - // (the `wp_guideline` CPT + `wp_guideline_type` taxonomy).
586 - // When that experiment isn't active the `wp/v2/guidelines`
587 - // + `wp/v2/wp_guideline_type` probes 404 — harmless but
588 - // noisy — so the shell skips booting the layer entirely.
589 - 'stickyNotes' => array(
590 - 'available' => openstation_sticky_notes_is_available(),
591 - ),
592 710 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
593 - 'aiSearchStreamUrl' => esc_url_raw( add_query_arg( 'action', 'openstation_ai_search_stream', admin_url( 'admin-ajax.php' ) ) ),
594 711 // AI assistant availability + per-user toggle. Drives whether the
595 712 // Cmd+K palette and admin-bar icon appear, and the setup placeholder.
596 713 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' )
597 714 ? openstation_ai_assistant_config()
@@ -604,41 +721,57 @@
604 721 // Site-wide games kill switch (Extended options). Exposed to
605 722 // every user — the shell skips the challenges Heartbeat
606 723 // channel when the framework is off.
607 724 'gamesEnabled' => openstation_games_enabled(),
608 - // Comments-window AI moderation toggle — surfaced at the
609 - // shell level so the OS Settings → Features tab can render
610 - // the toggle without depending on the Comments window
611 - // being registered for this user. URL is the same
612 - // endpoint the comments-window config exposes; state is
613 - // `null` for non-admins (the UI hides the row entirely).
614 - 'commentsAiUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/comments/ai-settings' ) ),
615 - // Non-null only for admins on a site where the Core AI stack is
616 - // present. Comment scoring routes through the AI Client (WP 7.0+),
617 - // so on older WordPress the whole row is hidden — same as the
618 - // assistant toggle — rather than shown disabled pointing at a
619 - // Settings → Connectors screen that doesn't exist there.
620 - 'commentsAi' => (
621 - current_user_can( 'manage_options' )
622 - && function_exists( 'openstation_ai_is_available' )
623 - && openstation_ai_is_available()
624 - )
625 - ? array(
626 - 'enabled' => function_exists( 'openstation_comments_ai_is_enabled' )
627 - ? openstation_comments_ai_is_enabled()
628 - : false,
629 - 'providerConfigured' => function_exists( 'openstation_comments_ai_provider_configured' )
630 - ? openstation_comments_ai_provider_configured()
631 - : false,
632 - )
633 - : null,
634 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(),
635 730 'portalUrl' => esc_url( openstation_portal_url() ),
636 731 'fromPortal' => $from_portal,
637 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,
638 738 'pwa' => array(
639 739 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
640 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(),
641 774 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
642 775 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
643 776 // Mirrors the manifest's `name` field — used by the
644 777 // install pill so the button reads "Install <site>"
@@ -654,8 +787,54 @@
654 787 // `src/pwa/sw-register.ts`). Default `false` preserves
655 788 // the polite behaviour where we yield to existing PWAs.
656 789 'forceReplaceSw' => openstation_pwa_force_replace_sw(),
657 790 ),
791 + // Ordered Core command-palette asset manifest, replayed on
792 + // first palette invocation. `null` on pre-6.9 sites.
793 + 'commandPalette' => $command_palette,
794 + // Stylesheets for shell surfaces that render on demand —
795 + // the Preferences panel, the AI assistant, the bug-report
796 + // window. None of them is a server-registered native
797 + // window (they are built client-side by the shell
798 + // bundle), so the `styles` companion mechanism can't
799 + // carry their CSS; instead the shell injects each sheet
800 + // the first time its surface opens, via
801 + // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
802 + // Same resolved shape a native window's `styleUrl` /
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(),
809 + 'deferredStyles' => openstation_build_deferred_styles(
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()
835 + )
836 + ),
658 837 )
659 838 );
660 839
661 840 wp_localize_script( 'openstation', 'openStationConfig', $config );
@@ -667,8 +846,46 @@
667 846 }
668 847 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' );
669 848
670 849 /**
850 + * Keep Core's boot-time command-palette enqueue off shell pages.
851 + *
852 + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on
853 + * `admin_enqueue_scripts` by default, which puts the palette's whole
854 + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on
855 + * every admin page. On a SHELL page that is pure dead weight: the
856 + * shell suppresses Core's palette unconditionally (the ⌘K keystroke
857 + * and the admin-bar icon both route to the shell's own palette), so
858 + * the runtime it powers can never be shown. Unhooking here lets the
859 + * deferred manifest (`openstation_build_command_palette_assets_payload()`)
860 + * capture the chain instead, and the shell loads it on the first
861 + * palette invocation.
862 + *
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.
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 + *
875 + * Priority 0, ahead of Core's default 10, so the removal lands
876 + * before the callback fires. On WP 6.9 (function exists, no default
877 + * hook) the `remove_action()` is a harmless no-op.
878 + */
879 +function openstation_defer_core_command_palette() {
880 + if ( ! openstation_is_shell_request() ) {
881 + return;
882 + }
883 + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
884 +}
885 +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 );
886 +
887 +/**
671 888 * Emits `<link rel="preload">` hints for the shell's critical-path
672 889 * assets so the browser starts fetching them as soon as it parses
673 890 * the document `<head>`.
674 891 *
@@ -711,14 +928,9 @@
711 928 * supply absolute URLs through the filter; in that case the consumer
712 929 * is responsible for the `crossorigin` semantics.
713 930 */
714 931 function openstation_print_preload_hints() {
715 - if (
716 - ! is_admin()
717 - || ! openstation_is_enabled()
718 - || openstation_is_chromeless_request()
719 - || openstation_is_classic_request()
720 - ) {
932 + if ( ! openstation_is_shell_request() ) {
721 933 return;
722 934 }
723 935
724 936 $suffix = openstation_asset_suffix();
@@ -761,8 +973,21 @@
761 973 'rel' => 'prefetch',
762 974 ),
763 975 );
764 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 +
765 990 /**
766 991 * Filters the list of resource preload hints emitted in `<head>`.
767 992 *
768 993 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -875,12 +1100,12 @@
875 1100 $deferred = apply_filters(
876 1101 'openstation_deferred_styles',
877 1102 array(
878 1103 'os-dock-peek',
1104 + 'os-openstation-layout',
879 1105 'desktop-mode-ai-assistant',
880 1106 'desktop-mode-bug-report',
881 1107 'os-window-overview',
882 - 'os-settings',
883 1108 )
884 1109 );
885 1110
886 1111 if ( ! in_array( $handle, (array) $deferred, true ) ) {