PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 All 37 releases
← All changes | includes/render/assets.php +353 -92 1.1.0 → 1.1.13 View file →
@@ -73,26 +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' );
91 + wp_enqueue_style( 'os-dock-taskbar' );
87 92 wp_enqueue_style( 'os-dock-peek' );
88 - wp_enqueue_style( 'os-notch' );
93 + wp_enqueue_style( 'os-workspaces' );
89 94 wp_enqueue_style( 'os-shortcuts' );
90 95 wp_enqueue_style( 'os-openstation-layout' );
91 - wp_enqueue_style( 'desktop-mode-ai-assistant' );
92 - wp_enqueue_style( 'desktop-mode-bug-report' );
93 96 wp_enqueue_style( 'os-files' );
94 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' );
95 101
96 102 // Solo mode — a single window freed into a native OS window by the
97 103 // desktop host. Same shell, everything but that one window hidden.
98 104 $solo_window = openstation_solo_window_id();
@@ -161,35 +167,35 @@
161 167 // WP normally only calls this on screens that opt in to the native
162 168 // palette; the shell needs it on every admin URL it might wrap.
163 169 // `function_exists` guard for pre-6.9 sites — the harvester gracefully
164 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 +
165 186 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 187 // Expose the same menu-commands array WP serializes into
183 188 // `wp.coreCommands.initializeCommandPalette(...)` on a window
184 189 // 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).
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.
192 198 $menu_map = openstation_build_command_menu_map();
193 199 wp_add_inline_script(
194 200 'openstation',
195 201 'window.__openStationMenuCommands = ' . wp_json_encode( $menu_map ) . ';',
@@ -197,9 +203,9 @@
197 203 );
198 204 }
199 205
200 206 // Pass configuration to JavaScript.
201 - global $title, $pagenow, $parent_file, $menu;
207 + global $title, $parent_file, $menu;
202 208
203 209 $menu_icon = 'dashicons-admin-generic';
204 210 if ( ! empty( $parent_file ) && ! empty( $menu ) ) {
205 211 foreach ( $menu as $item ) {
@@ -215,13 +221,32 @@
215 221 // plugin-contributed top-level routes. `openstation_dock_placement`
216 222 // is the per-item filter escape hatch for hiding. Shared with the
217 223 // REST menu endpoint so live refreshes (post plugin-activation)
218 224 // 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'] )
225 + $menu_payload = openstation_build_menu_payload();
226 + $dock_items = $menu_payload['dockItems'];
227 + $native_windows = isset( $menu_payload['nativeWindows'] )
222 228 ? $menu_payload['nativeWindows']
223 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();
224 249 $server_widgets = isset( $menu_payload['serverWidgets'] )
225 250 ? $menu_payload['serverWidgets']
226 251 : array();
227 252 $server_wallpapers = isset( $menu_payload['serverWallpapers'] )
@@ -288,9 +313,29 @@
288 313 // dedupes on an `activeId` that was never set.
289 314 $server_desktop_themes = isset( $menu_payload['serverDesktopThemes'] )
290 315 ? $menu_payload['serverDesktopThemes']
291 316 : array();
292 - $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'] )
293 338 ? $menu_payload['desktopIcons']
294 339 : array();
295 340
296 341 // Files-on-the-Desktop payload (Phase 0+1). Plugin-registered
@@ -301,8 +346,16 @@
301 346 : array();
302 347 $server_file_openers = function_exists( 'openstation_build_file_openers_payload' )
303 348 ? openstation_build_file_openers_payload()
304 349 : array();
350 + // Entries in the menu payload carry dependency handles; their
351 + // payloads ride once in `scriptDepPayloads` (GH#892). The file
352 + // lists stay whole: no sync module reads them on the client, and
353 + // compacting them here was the only write to the map after the
354 + // menu payload froze its own, so a refresh could not match it.
355 + $script_dep_payloads = isset( $menu_payload['scriptDepPayloads'] )
356 + ? (array) $menu_payload['scriptDepPayloads']
357 + : array();
305 358 $user_file_associations = function_exists( 'openstation_get_user_file_associations' )
306 359 ? openstation_get_user_file_associations( get_current_user_id() )
307 360 : array();
308 361 $server_wallpaper_menu_items = function_exists( 'openstation_build_wallpaper_menu_items' )
@@ -367,9 +420,9 @@
367 420 'maxSize' => $drop_max_size,
368 421 );
369 422
370 423 // Lazy-bundle URL builder. Each lazy-loaded bundle (AI Assistant,
371 - // About-scene, OS Settings panel, shell-overlays, window-system)
424 + // OS Settings panel, shell-overlays, window-system)
372 425 // is `<script>`-injected by the main bundle on demand — they don't
373 426 // go through `wp_register_script`, so they don't pick up WordPress's
374 427 // usual `?ver=<filemtime>` cache-buster. Without one, the browser
375 428 // happily serves a stale cached copy across plugin updates that
@@ -391,18 +444,33 @@
391 444 OPENSTATION_URL . 'assets/js/' . $base . $suffix . '.js?ver=' . $ver
392 445 );
393 446 };
394 447
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 ) : '' );
448 + // The page the shell opens first. On the shell screen it is the
449 + // validated `target` query arg (else the session's focused window,
450 + // the default window, the Dashboard); on a solo boot it is the
451 + // request's own URL. Either way the frozen portal flags are gone
452 + // from it, so the derived window id matches what the dock would
453 + // produce for the same page — otherwise auto-opening the entry
454 + // window and clicking the same dock icon would create a duplicate.
455 + $boot_target = openstation_shell_boot_target();
456 + $current_page = $boot_target['url'];
457 + $from_portal = $boot_target['fromPortal'];
458 + $from_portal_intent = $boot_target['fromPortalIntent'];
402 459
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
460 + // On the shell screen `$title` and `$parent_file` describe the
461 + // screen ("OpenStation", no menu), not the page about to open. The
462 + // dock entry for that page is the identity the entry window folds
463 + // into, so its title and icon are the right first paint; the iframe
464 + // reports its own title once it lands either way.
465 + $current_title = wp_strip_all_tags( (string) $title );
466 + if ( openstation_is_shell_screen_request() ) {
467 + $boot_meta = openstation_shell_boot_target_meta( $current_page, $dock_items );
468 + $current_title = wp_strip_all_tags( $boot_meta['title'] );
469 + if ( '' !== $boot_meta['icon'] ) {
470 + $menu_icon = $boot_meta['icon'];
471 + }
472 + }
405 473
406 474 /**
407 475 * Filters the desktop shell configuration passed to JavaScript.
408 476 *
@@ -434,8 +502,9 @@
434 502 * @type array $defaultWindow { enabled: bool, url: string } — current default-window preference.
435 503 * @type bool $canUpload Whether the user holds the `upload_files` capability.
436 504 * @type string $pluginUrl Plugin base URL (no trailing slash). Used by the shell to locate vendor assets and by plugins to build asset URLs.
437 505 * @type string $pluginVersion Plugin semver string. Surfaced in the OS Settings → About tab; plugins can read it to gate features by version.
506 + * @type string $aboutFeedUrl Authenticated admin-AJAX URL that returns the cached OpenStation journal feed for the About tab.
438 507 * @type string $restNonce Nonce for the session REST endpoint.
439 508 * @type string $soloWindow Window id when the shell was asked to paint exactly one window (`?openstation_solo=<id>`); '' otherwise. No dock, taskbar, wallpaper or desk, and no session restore.
440 509 * @type string $portalUrl Canonical `/openstation/` URL.
441 510 * @type bool $fromPortal Whether the shell was reached via the portal.
@@ -441,9 +510,14 @@
441 510 * @type bool $fromPortal Whether the shell was reached via the portal.
442 511 * @type bool $fromPortalIntent Whether the portal redirect resolved from an explicit `?target=…` (user navigation intent) rather than the session's focused window or the default-window fallback. Distinguishes a bare `/openstation/` visit from a portal-redirected admin-bar click so the shell can honour the URL the user actually asked for.
443 512 * @type array $seenIntros Slugs of one-time announcements the user has dismissed (e.g. `['openstation-rebrand']`).
444 513 * @type string $seenIntrosUrl REST endpoint for the seen-intros surface — POST `/seen` to mark, DELETE the base to reset.
514 + * @type bool $shellTour Whether this site offers the first-boot shell tour (`openstation_show_shell_tour`). Whether this user already had it is `seenIntros` containing `shell-tour`.
515 + * @type string $shellTourBundleUrl URL of the lazy shell-tour bundle, injected on first use.
516 + * @type array $firstRun `{ installedAt, firstEnabledAt, enabledAt }`, epoch seconds, 0 when unknown — the first-run stamps, read-only.
445 517 * @type bool $rebrandNotice Whether to offer this user the one-off announcement explaining the rename from Desktop Mode to OpenStation. True only when migration 5 flagged this user as a Desktop Mode user from before the rename AND they haven't dismissed the `openstation-rebrand` intro. Only ever present in the shell config, so the announcement never reaches the classic admin.
518 + * @type array|null $usageFeedback What the one-time usage feedback prompt needs (`restUrl`), or `null` when this user is not owed it: the feature is off, they have had OpenStation on for fewer than seven days by the `openstation_enabled_at` stamp, or they already answered or dismissed the `usage-feedback` intro. Carries no user data.
519 + * @type string $usageFeedbackBundleUrl URL of the lazy `usage-feedback` bundle, the form the prompt opens.
446 520 * }
447 521 */
448 522 $config = apply_filters(
449 523 'openstation_shell_config',
@@ -448,11 +522,15 @@
448 522 $config = apply_filters(
449 523 'openstation_shell_config',
450 524 array(
451 525 'currentPage' => esc_url( $current_page ),
452 - 'currentTitle' => wp_strip_all_tags( $title ),
526 + 'currentTitle' => $current_title,
453 527 'currentIcon' => sanitize_html_class( $menu_icon ),
454 - 'adminUrl' => esc_url( admin_url() ),
528 + // `self_admin_url()`: the base a window id is derived from,
529 + // and the URL the shell leaves for on exit. Both want the
530 + // admin the screen is in, which is the network one when the
531 + // network shell screen is what rendered.
532 + 'adminUrl' => esc_url( self_admin_url() ),
455 533 'homeUrl' => esc_url( home_url( '/' ) ),
456 534 // Decoded: the shell assigns this to `window.location`,
457 535 // where `&amp;` would make `_wpnonce` arrive as
458 536 // `amp;_wpnonce` and fail the nonce check.
@@ -465,8 +543,12 @@
465 543 // signature from this so the first off-allowlist menu change
466 544 // (vs. this boot state) is caught without a wasted probe. GH#325.
467 545 'menuSig' => isset( $menu_payload['menuSig'] ) ? (string) $menu_payload['menuSig'] : '',
468 546 'nativeWindows' => $native_windows,
547 + // Handle-keyed script data the entries above reference —
548 + // one copy per bundle, not one per window. See
549 + // `openstation_collect_native_windows_payload()`.
550 + 'nativeWindowScriptData' => $native_window_script_data,
469 551 'serverWidgets' => $server_widgets,
470 552 'serverWallpapers' => $server_wallpapers,
471 553 'serverCommandScripts' => $server_command_scripts,
472 554 'serverCommands' => $server_commands,
@@ -492,8 +574,11 @@
492 574 'serverDesktopThemes' => $server_desktop_themes,
493 575 'desktopIcons' => $desktop_icons,
494 576 'serverFileTypes' => $server_file_types,
495 577 'serverFileOpeners' => $server_file_openers,
578 + // Handle => dependency payload for every `scriptDeps` list in
579 + // this config; see `openstation_compact_script_deps()`.
580 + 'scriptDepPayloads' => (object) $script_dep_payloads,
496 581 'userFileAssociations' => $user_file_associations,
497 582 'filesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/files' ) ),
498 583 // Pinned-notes REST base (`includes/notes/rest.php`). The
499 584 // notes layer boots only when this is present.
@@ -509,9 +594,17 @@
509 594 'coreNotices' => openstation_get_core_notices(),
510 595 'pluginNotices' => openstation_get_plugin_notices(),
511 596 'defaultWallpaper' => openstation_get_default_wallpaper(),
512 597 'session' => openstation_get_session( get_current_user_id() ),
513 - 'sessionUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/session' ) ),
598 + // The session route runs in the main site's blog context
599 + // whichever desktop posts to it, so the network screen's
600 + // URL says which session it is addressing — see
601 + // `openstation_rest_session_network()`.
602 + 'sessionUrl' => esc_url_raw(
603 + is_network_admin()
604 + ? add_query_arg( 'network', '1', rest_url( 'desktop-mode/v1/session' ) )
605 + : rest_url( 'desktop-mode/v1/session' )
606 + ),
514 607 'restUrl' => esc_url_raw( rest_url() ),
515 608 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ),
516 609 'dropConfig' => $drop_config,
517 610 'defaultWindowUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/default-window' ) ),
@@ -518,8 +611,17 @@
518 611 'defaultWindow' => openstation_get_default_window( get_current_user_id() ),
519 612 'canUpload' => current_user_can( 'upload_files' ),
520 613 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ),
521 614 'pluginVersion' => OPENSTATION_VERSION,
615 + 'aboutFeedUrl' => esc_url_raw(
616 + add_query_arg(
617 + array(
618 + 'action' => 'openstation_about_feed',
619 + 'nonce' => wp_create_nonce( 'openstation_about_feed' ),
620 + ),
621 + admin_url( 'admin-ajax.php' )
622 + )
623 + ),
522 624 'iframeBridgeUrl' => $lazy_bundle_url( 'iframe-bridge' ),
523 625 // URL of the AI Assistant lazy bundle. The main bundle
524 626 // ships a stub matching the public `wp.os.ai` API; the
525 627 // stub `<script>`-injects this URL the first time the user
@@ -525,25 +627,39 @@
525 627 // stub `<script>`-injects this URL the first time the user
526 628 // opens the assistant. Picking `.js` vs `.min.js` here keeps
527 629 // the SCRIPT_DEBUG gate server-side, matching iframeBridgeUrl.
528 630 'aiAssistantBundleUrl' => $lazy_bundle_url( 'ai-assistant' ),
529 - // URL of the About-scene lazy bundle. The OS Settings →
530 - // About tab loads this on first mount; ~25 kB PixiJS
531 - // particle scene that would otherwise ship in the main
532 - // bundle for every shell load.
533 - 'aboutSceneBundleUrl' => $lazy_bundle_url( 'about-scene' ),
534 - // URL of the OS Settings panel lazy bundle. Injected by
535 - // the main bundle's `OsSettings.renderPanel()` stub on
536 - // the user's first Settings open. Holds every section
537 - // renderer + the `<os-*>` components only the panel
538 - // uses, so nothing about Settings ships in
539 - // `desktop.min.js` for users who never open it.
540 - 'osSettingsPanelBundleUrl' => $lazy_bundle_url( 'os-settings-panel' ),
541 631 // URL of the shell-overlays lazy bundle. Pre-loaded by
542 632 // the main bundle after first paint so action-triggered
543 633 // overlays (toast, confirm dialog, context menus) feel
544 634 // instant the first time they fire.
545 635 'shellOverlaysBundleUrl' => $lazy_bundle_url( 'shell-overlays' ),
636 + // The shell-bundle diet: features whose right moment is a
637 + // user gesture (or a presence signal) ride their own
638 + // bundles instead of the boot-critical `desktop[.min].js`.
639 + // Each sentinel in the shell loads its bundle at that
640 + // moment — see the entry file each bundle names.
641 + 'fileDropBundleUrl' => $lazy_bundle_url( 'file-drop' ),
642 + 'filesOverlaysBundleUrl' => $lazy_bundle_url( 'files-overlays' ),
643 + 'notesBundleUrl' => $lazy_bundle_url( 'notes' ),
644 + 'dockConstellationBundleUrl' => $lazy_bundle_url( 'dock-constellation' ),
645 + 'windowLinkVisualsBundleUrl' => $lazy_bundle_url( 'window-link-visuals' ),
646 + // Presence hint for the notes sentinel: a desktop with no
647 + // notes skips the notes bundle AND the boot-time list
648 + // request. Two id-only existence probes at most.
649 + 'hasNotes' => function_exists( 'openstation_notes_user_has_any' )
650 + ? openstation_notes_user_has_any()
651 + : false,
652 + // URL of the full `<os-*>` component kit. The shell
653 + // never loads this — its own bundles import the
654 + // components they render. It exists for
655 + // `wp.os.loadComponents()`, i.e. for plugin code that
656 + // CANNOT import: a plugin shipped as a zip has no path
657 + // to this repo at build time, so before this URL its
658 + // only routes to a `<os-switch>` were to bundle a second
659 + // copy or hand-roll one. Shipping the URL costs one
660 + // string and keeps the SCRIPT_DEBUG choice server-side.
661 + 'componentsBundleUrl' => $lazy_bundle_url( 'os-components' ),
546 662 // Mio — the desk companion. `mio` carries the
547 663 // appearance + physics (see `openstation_mio_config()`);
548 664 // `mioBundleUrl` is the lazy PixiJS bundle the shell
549 665 // controller injects the first time a user switches the
@@ -567,12 +683,29 @@
567 683 // / `openNew()` call (both async); pre-loaded
568 684 // by the shell after first paint when no session is being
569 685 // restored and no `openCurrentPage` will fire.
570 686 'windowSystemBundleUrl' => $lazy_bundle_url( 'window-system' ),
687 + // URL of the lazy phone-layer bundle. Injected by the main
688 + // bundle only when the mode resolves to `mobile`, so a
689 + // desktop never fetches it. `mode` carries the preference
690 + // and breakpoints the first-paint head stamp already used,
691 + // plus the server's default tab-bar pins — see
692 + // `includes/mobile.php`.
693 + 'mobileBundleUrl' => $lazy_bundle_url( 'mobile' ),
694 + 'mode' => openstation_mode_config( get_current_user_id() ),
571 695 // URL of the item-visibility-menu lazy bundle — the
572 696 // right-click "hide from dock / desktop" menu. Injected by
573 697 // the main bundle's loader shim on the first right-click.
574 698 'itemVisibilityMenuBundleUrl' => $lazy_bundle_url( 'item-visibility-menu' ),
699 + // URL of the workspace-wizard lazy bundle — the modal
700 + // behind "Edit this workspace…". Injected by the main
701 + // bundle's loader shim on first open.
702 + 'workspaceWizardBundleUrl' => $lazy_bundle_url( 'workspace-wizard' ),
703 + // Server-side view of the workspace templates, so a plugin
704 + // can add or drop one from PHP. The client merges these
705 + // with its own built-ins by id — see
706 + // `src/workspaces/server-sync.ts`.
707 + 'workspacePresets' => openstation_workspace_presets(),
575 708 // URL of the release-card lazy bundle — the vinyl core-
576 709 // update announcement. Injected by `maybeShowUpdate()` only
577 710 // when a core update is actually pending.
578 711 'releaseCardBundleUrl' => $lazy_bundle_url( 'release-card' ),
@@ -589,8 +722,26 @@
589 722 // announcement yet. Same value that gated `os-announce`
590 723 // above; the dialog cannot paint without that stylesheet, so
591 724 // the two must not diverge.
592 725 'rebrandNotice' => $show_rebrand_notice,
726 + // The first-boot shell tour: whether this site offers it
727 + // (the `openstation_show_shell_tour` filter), and the lazy
728 + // bundle that runs it. Whether THIS user already had it is
729 + // `seenIntros` containing `shell-tour`; the shell reads that
730 + // itself so a reset can replay the tour without a new boot.
731 + 'shellTour' => openstation_should_offer_shell_tour( get_current_user_id() ),
732 + 'shellTourBundleUrl' => $lazy_bundle_url( 'shell-tour' ),
733 + // The first-run stamps, read-only, epoch seconds (0 when
734 + // unknown): when the plugin was installed, when anyone first
735 + // enabled it, and when this user did. See
736 + // `includes/first-run/stamps.php`.
737 + 'firstRun' => openstation_first_run_config( get_current_user_id() ),
738 + // Null for everyone but a user who has had OpenStation on
739 + // long enough and has not answered yet; the gate lives in
740 + // `includes/feedback/usage.php`. The form is a lazy bundle
741 + // fetched only when the user says yes to the prompt.
742 + 'usageFeedback' => function_exists( 'openstation_usage_feedback_config' ) ? openstation_usage_feedback_config() : null,
743 + 'usageFeedbackBundleUrl' => $lazy_bundle_url( 'usage-feedback' ),
593 744 'aiSearchUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/search' ) ),
594 745 // AI assistant availability + per-user toggle. Drives whether the
595 746 // Cmd+K palette and admin-bar icon appear, and the setup placeholder.
596 747 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' )
@@ -599,46 +750,65 @@
599 750 // Lets the Features tab re-check provider availability without a
600 751 // reload after a connector is configured in Settings → Connectors.
601 752 'aiStatusUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/ai/status' ) ),
602 753 'extendedOptions' => current_user_can( 'manage_options' ) ? openstation_get_extended_options() : null,
754 + // Agents offered as "Send to <agent>" on desktop and folder
755 + // tiles. See `src/desktop-files/agent-menu-items.ts`.
756 + 'agentsSendTo' => function_exists( 'openstation_agents_send_to_targets' ) ? openstation_agents_send_to_targets() : array(),
603 757 'extendedOptionsUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/extended-options' ) ),
604 758 // Site-wide games kill switch (Extended options). Exposed to
605 759 // every user — the shell skips the challenges Heartbeat
606 760 // channel when the framework is off.
607 761 '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 762 'currentUserIsAdmin' => current_user_can( 'manage_options' ),
763 + // Null on single-site installs; its `networkAdmin` null
764 + // without `manage_network`, which is what keeps the dock
765 + // tile from registering.
766 + 'multisite' => openstation_multisite_payload(),
635 767 'portalUrl' => esc_url( openstation_portal_url() ),
636 768 'fromPortal' => $from_portal,
637 769 'fromPortalIntent' => $from_portal_intent,
770 + // One-shot like the boot target: a switch from another
771 + // site's overview lands in this one's.
772 + 'landInOverview' => openstation_shell_lands_in_overview(),
773 + 'arrivalDirection' => openstation_shell_arrival_direction(),
774 + 'hopLinkOffer' => function_exists( 'openstation_network_link_offer' ) ? openstation_network_link_offer() : null,
638 775 'pwa' => array(
639 776 'manifestUrl' => esc_url_raw( openstation_pwa_manifest_url() ),
640 777 'swUrl' => esc_url_raw( openstation_pwa_sw_url() ),
778 + // Extensionless retry target for hosts whose nginx 404s
779 + // virtual .js paths before WordPress runs (WordPress.com).
780 + 'swFallbackUrl' => esc_url_raw( openstation_pwa_sw_fallback_url() ),
781 + // The site's home path — the scope the registration
782 + // asks for, so a subdirectory network's sites each get
783 + // their own worker instead of fighting over the root.
784 + 'swScope' => openstation_pwa_sw_scope(),
785 + // The worker's per-user flags, computed HERE rather than
786 + // baked into the served `sw.js`.
787 + //
788 + // A service worker is origin-wide but these are per-user
789 + // preferences, so putting them in the script bytes made
790 + // the body differ between an anonymous and a logged-in
791 + // request — and any in-scope logged-out navigation then
792 + // installed a "new" worker, which at the time reloaded
793 + // the shell. The bytes are identical for everyone now;
794 + // the shell posts these to the worker at boot.
795 + //
796 + // Computed server-side, not read from the settings
797 + // snapshot client-side, because
798 + // `openstation_pwa_admin_asset_cache_enabled()` applies
799 + // the `openstation_pwa_admin_asset_cache` filter — an
800 + // operator's site-wide veto has to keep working.
801 + 'swConfig' => array(
802 + 'adminAssetCache' => (bool) openstation_pwa_admin_asset_cache_enabled(),
803 + 'windowPrewarm' => ! empty( openstation_get_os_settings( get_current_user_id() )['windowPrewarmEnabled'] ),
804 + ),
805 + // The build this shell document belongs to. When a new
806 + // worker takes over mid-session the shell asks it for
807 + // the stamp it was served with and compares; only a
808 + // difference — the shell's own files changed on the
809 + // server — offers the user a reload. Never automatic.
810 + 'shellBuild' => openstation_shell_build_stamp(),
641 811 'stateUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/pwa-state' ) ),
642 812 'state' => openstation_pwa_get_user_state( get_current_user_id() ),
643 813 // Mirrors the manifest's `name` field — used by the
644 814 // install pill so the button reads "Install <site>"
@@ -654,8 +824,54 @@
654 824 // `src/pwa/sw-register.ts`). Default `false` preserves
655 825 // the polite behaviour where we yield to existing PWAs.
656 826 'forceReplaceSw' => openstation_pwa_force_replace_sw(),
657 827 ),
828 + // Ordered Core command-palette asset manifest, replayed on
829 + // first palette invocation. `null` on pre-6.9 sites.
830 + 'commandPalette' => $command_palette,
831 + // Stylesheets for shell surfaces that render on demand —
832 + // the Preferences panel, the AI assistant, the bug-report
833 + // window. None of them is a server-registered native
834 + // window (they are built client-side by the shell
835 + // bundle), so the `styles` companion mechanism can't
836 + // carry their CSS; instead the shell injects each sheet
837 + // the first time its surface opens, via
838 + // `ensureDeferredStyle()` in `src/deferred-styles.ts`.
839 + // Same resolved shape a native window's `styleUrl` /
840 + // `styleInline` travels in.
841 + // Which of the `deferredStyles` entries a game needs.
842 + // `launchGame()` injects these before the window paints.
843 + 'gameStyleHandles' => function_exists( 'openstation_games_style_handles' )
844 + ? openstation_games_style_handles()
845 + : array(),
846 + 'deferredStyles' => openstation_build_deferred_styles(
847 + array_merge(
848 + array(
849 + 'desktop-mode-ai-assistant',
850 + 'desktop-mode-bug-report',
851 + // The explorer's shared sheet. It rides the WP
852 + // Explorer APP as a companion style, but the
853 + // desktop FOLDER window paints its preview pane
854 + // with the same `os-my-wordpress__*` classes and
855 + // — being a native window opened straight from
856 + // JS — carries no companion styles of its own.
857 + // Without this, the pane rendered unstyled until
858 + // the explorer had been opened once in the
859 + // session.
860 + 'desktop-mode-my-wordpress',
861 + ),
862 + // The Games sheets. They also ride the hub window as
863 + // companion styles, but a game is reachable without
864 + // the hub — the challenge toast, solo mode, and
865 + // `wp.os.games.launch()` all land in `launchGame()`
866 + // with no hub window in the tab. Listing them here
867 + // costs a URL each in the boot config and no CSS
868 + // until `launchGame()` asks.
869 + function_exists( 'openstation_games_style_handles' )
870 + ? openstation_games_style_handles()
871 + : array()
872 + )
873 + ),
658 874 )
659 875 );
660 876
661 877 wp_localize_script( 'openstation', 'openStationConfig', $config );
@@ -667,8 +883,46 @@
667 883 }
668 884 add_action( 'admin_enqueue_scripts', 'openstation_enqueue_assets' );
669 885
670 886 /**
887 + * Keep Core's boot-time command-palette enqueue off shell pages.
888 + *
889 + * WordPress 7.0 hooks `wp_enqueue_command_palette_assets()` on
890 + * `admin_enqueue_scripts` by default, which puts the palette's whole
891 + * dependency chain — the Gutenberg runtime, ~800 KB gzipped — on
892 + * every admin page. On a SHELL page that is pure dead weight: the
893 + * shell suppresses Core's palette unconditionally (the ⌘K keystroke
894 + * and the admin-bar icon both route to the shell's own palette), so
895 + * the runtime it powers can never be shown. Unhooking here lets the
896 + * deferred manifest (`openstation_build_command_palette_assets_payload()`)
897 + * capture the chain instead, and the shell loads it on the first
898 + * palette invocation.
899 + *
900 + * Deliberately scoped: classic-mode requests keep Core's default,
901 + * because a classic page is Core's own UI where Core's palette is the
902 + * right one.
903 + *
904 + * Windows are handled separately by
905 + * {@see openstation_chromeless_should_trim_command_palette()} in
906 + * `includes/render/chromeless-trim.php` — same idea, but it has to
907 + * drop the whole palette *family* rather than just unhook Core's
908 + * callback, and it exempts block-editor screens. Unhooking alone is
909 + * not enough there: a third-party palette extension that declares
910 + * `wp-commands` keeps the entire chain queued as its dependency.
911 + *
912 + * Priority 0, ahead of Core's default 10, so the removal lands
913 + * before the callback fires. On WP 6.9 (function exists, no default
914 + * hook) the `remove_action()` is a harmless no-op.
915 + */
916 +function openstation_defer_core_command_palette() {
917 + if ( ! openstation_is_shell_request() ) {
918 + return;
919 + }
920 + remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
921 +}
922 +add_action( 'admin_enqueue_scripts', 'openstation_defer_core_command_palette', 0 );
923 +
924 +/**
671 925 * Emits `<link rel="preload">` hints for the shell's critical-path
672 926 * assets so the browser starts fetching them as soon as it parses
673 927 * the document `<head>`.
674 928 *
@@ -711,14 +965,9 @@
711 965 * supply absolute URLs through the filter; in that case the consumer
712 966 * is responsible for the `crossorigin` semantics.
713 967 */
714 968 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 - ) {
969 + if ( ! openstation_is_shell_request() ) {
721 970 return;
722 971 }
723 972
724 973 $suffix = openstation_asset_suffix();
@@ -761,8 +1010,21 @@
761 1010 'rel' => 'prefetch',
762 1011 ),
763 1012 );
764 1013
1014 + // The phone layer is needed at boot on a phone and never on a
1015 + // desktop; the server cannot see the viewport, so the user agent
1016 + // decides whether the hint is worth its bytes. A wrong guess costs
1017 + // one low-priority fetch, never a wrong layout — the stamp and the
1018 + // bundle loader read the real viewport.
1019 + if ( openstation_mode_hint_is_mobile( get_current_user_id() ) ) {
1020 + $hints[] = array(
1021 + 'href' => $build_url( 'assets/js/mobile' . $suffix . '.js' ),
1022 + 'as' => 'script',
1023 + 'rel' => 'prefetch',
1024 + );
1025 + }
1026 +
765 1027 /**
766 1028 * Filters the list of resource preload hints emitted in `<head>`.
767 1029 *
768 1030 * Each entry is a `{ 'href' => string, 'as' => string,
@@ -879,9 +1141,8 @@
879 1141 'os-openstation-layout',
880 1142 'desktop-mode-ai-assistant',
881 1143 'desktop-mode-bug-report',
882 1144 'os-window-overview',
883 - 'os-settings',
884 1145 )
885 1146 );
886 1147
887 1148 if ( ! in_array( $handle, (array) $deferred, true ) ) {