initial
This commit is contained in:
+998
@@ -0,0 +1,998 @@
|
||||
<?php
|
||||
/**
|
||||
* Agents manager
|
||||
*
|
||||
* @package automattic/jetpack-agents-manager
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Agents_Manager;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Class Agents_Manager
|
||||
*/
|
||||
class Agents_Manager {
|
||||
/**
|
||||
* The package version of the Agents Manager package.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PACKAGE_VERSION = '0.6.0';
|
||||
|
||||
/**
|
||||
* Help Center URL for disconnected variants.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const HELP_CENTER_URL = 'https://wordpress.com/help?help-center=home';
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Agents_Manager
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Agents_Manager constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_api' ) );
|
||||
add_filter( 'calypso_preferences_update', array( $this, 'calypso_preferences_update' ) );
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
|
||||
add_action( 'next_admin_init', array( $this, 'enqueue_scripts' ), 1001 );
|
||||
add_filter( 'agents_manager_use_unified_experience', array( $this, 'should_use_unified_experience' ) );
|
||||
|
||||
Sidebar_Open_Preservation::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the agents manager menu panel should be displayed.
|
||||
*
|
||||
* @return bool True if the menu panel should be displayed.
|
||||
*/
|
||||
public function should_display_menu_panel() {
|
||||
return self::is_unified_experience();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SVG icon markup for a given icon name.
|
||||
*
|
||||
* @param string $icon_name The name of the icon to retrieve.
|
||||
* @return string The SVG markup.
|
||||
*/
|
||||
private function get_icon( $icon_name ) {
|
||||
$icons = array(
|
||||
'comment' => '<svg class="help-center-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z" /></svg>',
|
||||
'backup' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.5 12h1.75l-2.5 3-2.5-3H4a8 8 0 113.134 6.35l.907-1.194A6.5 6.5 0 105.5 12zm9.53 1.97l-2.28-2.28V8.5a.75.75 0 00-1.5 0V12a.747.747 0 00.218.529l1.282-.84-1.28.842 2.5 2.5a.75.75 0 101.06-1.061z" /></svg>',
|
||||
'page' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.5 7.5h-7V9h7V7.5Zm-7 3.5h7v1.5h-7V11Zm7 3.5h-7V16h7v-1.5Z" /><path d="M17 4H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM7 5.5h10a.5.5 0 0 1 .5.5v12a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V6a.5.5 0 0 1 .5-.5Z" /></svg>',
|
||||
'video' => '<svg class="help-center-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.7 3H5.3C4 3 3 4 3 5.3v13.4C3 20 4 21 5.3 21h13.4c1.3 0 2.3-1 2.3-2.3V5.3C21 4 20 3 18.7 3zm.8 15.7c0 .4-.4.8-.8.8H5.3c-.4 0-.8-.4-.8-.8V5.3c0-.4.4-.8.8-.8h13.4c.4 0 .8.4.8.8v13.4zM10 15l5-3-5-3v6z" /></svg>',
|
||||
'rss' => '<svg class="help-center-menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 10.2h-.8v1.5H5c1.9 0 3.8.8 5.1 2.1 1.4 1.4 2.1 3.2 2.1 5.1v.8h1.5V19c0-2.3-.9-4.5-2.6-6.2-1.6-1.6-3.8-2.6-6.1-2.6zm10.4-1.6C12.6 5.8 8.9 4.2 5 4.2h-.8v1.5H5c3.5 0 6.9 1.4 9.4 3.9s3.9 5.8 3.9 9.4v.8h1.5V19c0-3.9-1.6-7.6-4.4-10.4zM4 20h3v-3H4v3z" /></svg>',
|
||||
);
|
||||
|
||||
return $icons[ $icon_name ] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Agents Manager Help "?" node (`agents-manager`) to the admin bar, replacing the
|
||||
* legacy Help Center node (`help-center`).
|
||||
*
|
||||
* @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
|
||||
* @param bool $use_disconnected Disconnected variants link straight to the Help Center instead of opening the dropdown.
|
||||
*/
|
||||
public function add_help_menu( $wp_admin_bar, $use_disconnected ) {
|
||||
$wp_admin_bar->remove_node( 'help-center' );
|
||||
|
||||
$menu_args = array(
|
||||
'id' => 'agents-manager',
|
||||
'title' => '<span title="' . esc_attr__( 'Help Center', 'jetpack-agents-manager' ) . '"><svg id="agents-manager-icon" class="ab-icon" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm-1 16v-2h2v2h-2zm2-3v-1.141A3.991 3.991 0 0016 10a4 4 0 00-8 0h2c0-1.103.897-2 2-2s2 .897 2 2-.897 2-2 2a1 1 0 00-1 1v2h2z" />
|
||||
</svg></span>',
|
||||
'parent' => 'top-secondary',
|
||||
);
|
||||
|
||||
if ( $use_disconnected ) {
|
||||
$menu_args['href'] = self::HELP_CENTER_URL;
|
||||
$menu_args['meta'] = array(
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
);
|
||||
} else {
|
||||
$menu_args['meta'] = array(
|
||||
'html' => '<div id="agents-manager-masterbar"></div>',
|
||||
'class' => 'menupop',
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
);
|
||||
}
|
||||
|
||||
$wp_admin_bar->add_menu( $menu_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the agents manager menu panel to the admin bar.
|
||||
*
|
||||
* @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
|
||||
*/
|
||||
public function add_menu_panel( $wp_admin_bar ) {
|
||||
// Add chat support group
|
||||
$wp_admin_bar->add_group(
|
||||
array(
|
||||
'parent' => 'agents-manager',
|
||||
'id' => 'agents-manager-menu-panel-chat',
|
||||
'meta' => array(
|
||||
'class' => 'ab-sub-secondary',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Add chat support menu item
|
||||
$wp_admin_bar->add_node(
|
||||
array(
|
||||
'parent' => 'agents-manager-menu-panel-chat',
|
||||
'id' => 'agents-manager-chat-support',
|
||||
'title' => $this->get_icon( 'comment' ) . '<span>' . __( 'Chat support', 'jetpack-agents-manager' ) . '</span>',
|
||||
)
|
||||
);
|
||||
|
||||
// Add chat history menu item
|
||||
$wp_admin_bar->add_node(
|
||||
array(
|
||||
'parent' => 'agents-manager-menu-panel-chat',
|
||||
'id' => 'agents-manager-chat-history',
|
||||
'title' => $this->get_icon( 'backup' ) . '<span>' . __( 'Chat history', 'jetpack-agents-manager' ) . '</span>',
|
||||
)
|
||||
);
|
||||
|
||||
// Add links group
|
||||
$wp_admin_bar->add_group(
|
||||
array(
|
||||
'parent' => 'agents-manager',
|
||||
'id' => 'agents-manager-menu-panel-links',
|
||||
'meta' => array(
|
||||
'class' => 'ab-sub-secondary',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Add support guides menu item
|
||||
$wp_admin_bar->add_node(
|
||||
array(
|
||||
'parent' => 'agents-manager-menu-panel-links',
|
||||
'id' => 'agents-manager-support-guides',
|
||||
'title' => $this->get_icon( 'page' ) . '<span>' . __( 'Support guides', 'jetpack-agents-manager' ) . '</span>',
|
||||
)
|
||||
);
|
||||
|
||||
// Add courses menu item
|
||||
$wp_admin_bar->add_node(
|
||||
array(
|
||||
'parent' => 'agents-manager-menu-panel-links',
|
||||
'id' => 'agents-manager-courses',
|
||||
'title' => $this->get_icon( 'video' ) . '<span>' . __( 'Courses', 'jetpack-agents-manager' ) . '</span>',
|
||||
'href' => 'https://wordpress.com/support/courses/',
|
||||
'meta' => array(
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Add product updates menu item
|
||||
$wp_admin_bar->add_node(
|
||||
array(
|
||||
'parent' => 'agents-manager-menu-panel-links',
|
||||
'id' => 'agents-manager-product-updates',
|
||||
'title' => $this->get_icon( 'rss' ) . '<span>' . __( 'Product updates', 'jetpack-agents-manager' ) . '</span>',
|
||||
'href' => 'https://wordpress.com/blog/category/product-features/',
|
||||
'meta' => array(
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the standalone AI chat button to the admin bar.
|
||||
*
|
||||
* @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
|
||||
*/
|
||||
public function add_ai_chat_button( $wp_admin_bar ) {
|
||||
$wp_admin_bar->add_menu(
|
||||
array(
|
||||
'id' => 'agents-manager-ai-chat',
|
||||
'parent' => 'top-secondary',
|
||||
'title' => '<span title="' . esc_attr__( 'Ask AI', 'jetpack-agents-manager' ) . '"><svg class="ab-icon" role="img" aria-label="' . esc_attr__( 'Ask AI', 'jetpack-agents-manager' ) . '" width="24" height="24" viewBox="-45 -45 490 490" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M391.528 188.061L309.455 159.75C276.997 148.597 251.403 123.003 240.25 90.5451L211.939 8.47185C208.079 -2.82395 191.921 -2.82395 188.061 8.47185L159.75 90.5451C148.597 123.003 123.003 148.597 90.5451 159.75L8.47185 188.061C-2.82395 191.921 -2.82395 208.079 8.47185 211.939L90.5451 240.25C123.003 251.403 148.597 276.997 159.75 309.455L188.061 391.528C191.921 402.824 208.079 402.824 211.939 391.528L240.25 309.455C251.403 276.997 276.997 251.403 309.455 240.25L391.528 211.939C402.824 208.079 402.824 191.921 391.528 188.061ZM295.728 206.077L254.692 220.232C238.391 225.809 225.666 238.677 220.089 254.835L205.934 295.871C203.932 301.591 195.925 301.591 193.923 295.871L179.768 254.835C174.191 238.534 161.323 225.809 145.165 220.232L104.129 206.077C98.4093 204.075 98.4093 196.068 104.129 194.066L145.165 179.911C161.466 174.334 174.191 161.466 179.768 145.308L193.923 104.272C195.925 98.5523 203.932 98.5523 205.934 104.272L220.089 145.308C225.666 161.609 238.534 174.334 254.692 179.911L295.728 194.066C301.448 196.068 301.448 204.075 295.728 206.077Z" />
|
||||
</svg></span>',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Agents Manager scripts and add inline script data.
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
// Early return for P2 frontend - don't add admin bar or enqueue scripts.
|
||||
$stylesheet = get_stylesheet();
|
||||
$is_p2 = str_contains( $stylesheet, 'pub/p2' ) || function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( get_current_blog_id() );
|
||||
|
||||
if ( ! is_admin() && $is_p2 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine which variant to load (null = don't load).
|
||||
$variant = self::get_active_variant();
|
||||
if ( null === $variant ) {
|
||||
return;
|
||||
}
|
||||
$use_disconnected = str_contains( $variant, 'disconnected' );
|
||||
$is_gutenberg = $this->is_block_editor();
|
||||
|
||||
// In Gutenberg, dequeue Help Center so we don't end up with two buttons — but only
|
||||
// in the full unified experience, where Agents Manager takes over the Help Center.
|
||||
// In block-editor-only mode (e.g. ?flags=unified-big-sky) Agents Manager replaces
|
||||
// Big Sky's native UI and Help Center should remain available.
|
||||
// Agents Manager fires at priority 101, after Help Center at 100, so HC is already enqueued.
|
||||
if ( $is_gutenberg && self::is_unified_experience() ) {
|
||||
wp_dequeue_script( 'help-center' );
|
||||
wp_dequeue_style( 'help-center-style' );
|
||||
}
|
||||
|
||||
// For non-Gutenberg, non-CIAB environments, add to the admin bar. The fullscreen Gutenberg
|
||||
// editor has no admin bar, so JS handles UI insertion — except under the omnibar, which is
|
||||
// handled below. CIAB hides the admin bar and uses its own Site Hub.
|
||||
$is_ciab = $this->is_ciab_environment();
|
||||
if ( ! $is_gutenberg && ! $is_ciab ) {
|
||||
add_action(
|
||||
'admin_bar_menu',
|
||||
function ( $wp_admin_bar ) use ( $use_disconnected ) {
|
||||
$this->add_help_menu( $wp_admin_bar, $use_disconnected );
|
||||
},
|
||||
// Add the agents manager icon to the admin bar after the help center is added, so we can remove it.
|
||||
100
|
||||
);
|
||||
|
||||
// Initialize the agents manager menu panel (only for full variants, not disconnected)
|
||||
if ( ! $use_disconnected ) {
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_menu_panel' ), 100 );
|
||||
}
|
||||
|
||||
// Standalone AI chat button, shown only in the unified experience.
|
||||
if ( ! $use_disconnected && self::is_unified_experience() ) {
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_ai_chat_button' ), 100 );
|
||||
}
|
||||
}
|
||||
|
||||
// When Gutenberg's "admin bar in editor" (omnibar) experiment is active, expose the entry
|
||||
// points in that editor admin bar (CIAB is excluded — it has its own Site Hub UI). The Help
|
||||
// "?" dropdown shows only in the full unified experience (mirroring wp-admin); the Ask AI
|
||||
// button shows in any dev/internal context while the feature is in development. The
|
||||
// wp-calypso admin-bar integration wires both, so no frontend change is needed.
|
||||
if ( ! $is_ciab && ! $use_disconnected && self::is_admin_bar_in_editor() ) {
|
||||
// Help "?" node + dropdown panel first, matching the wp-admin admin bar order.
|
||||
if ( self::is_unified_experience() ) {
|
||||
add_action(
|
||||
'admin_bar_menu',
|
||||
function ( $wp_admin_bar ) {
|
||||
$this->add_help_menu( $wp_admin_bar, false );
|
||||
},
|
||||
100
|
||||
);
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_menu_panel' ), 100 );
|
||||
}
|
||||
|
||||
// Ask AI button — dev/internal contexts only while the feature is in development.
|
||||
if ( self::is_dev_mode() ) {
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_ai_chat_button' ), 100 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to register agent provider modules for the Agents Manager.
|
||||
*
|
||||
* Plugins can hook into this filter to register script module IDs that export
|
||||
* toolProvider and/or contextProvider. The Agents Manager JS will dynamically
|
||||
* import these modules and merge their providers.
|
||||
*
|
||||
* @param array $providers Array of provider script module IDs.
|
||||
*/
|
||||
$agent_providers = apply_filters( 'agents_manager_agent_providers', array() );
|
||||
|
||||
$use_unified_experience = self::is_unified_experience();
|
||||
|
||||
/**
|
||||
* Filter the default agent ID for the Agents Manager.
|
||||
*
|
||||
* Allows host applications (e.g., CIAB, WooCommerce AI) to specify a custom
|
||||
* workflow agent instead of the default orchestrator. The value is passed to
|
||||
* the frontend as `agentsManagerData.agentId` and consumed by `useAgentConfig()`.
|
||||
*
|
||||
* @param string|null $agent_id The agent ID to use, or null for default behavior.
|
||||
*/
|
||||
$agent_id = apply_filters( 'agents_manager_agent_id', null );
|
||||
|
||||
$this->enqueue_script( $variant );
|
||||
|
||||
$inline_data = array(
|
||||
'agentProviders' => $agent_providers,
|
||||
'useUnifiedExperience' => $use_unified_experience,
|
||||
'isDevMode' => self::is_dev_mode(),
|
||||
'sectionName' => apply_filters( 'agents_manager_section_name', $variant ),
|
||||
'currentUser' => $this->get_current_user_data(),
|
||||
'site' => $this->get_current_site(),
|
||||
'helpCenterUrl' => self::HELP_CENTER_URL,
|
||||
);
|
||||
|
||||
if ( $agent_id ) {
|
||||
$inline_data['agentId'] = $agent_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the data exposed to the Agents Manager frontend.
|
||||
*
|
||||
* @param array $inline_data Data encoded into `agentsManagerData`.
|
||||
*/
|
||||
$filtered = apply_filters( 'jetpack_ai_sidebar_agents_manager_data', $inline_data );
|
||||
$inline_data = is_array( $filtered ) ? $filtered : $inline_data;
|
||||
|
||||
wp_add_inline_script(
|
||||
'agents-manager',
|
||||
'const agentsManagerData = ' . wp_json_encode(
|
||||
$inline_data,
|
||||
JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
|
||||
) . ';',
|
||||
'before'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The script variant active for this request, or null if none.
|
||||
*
|
||||
* Single source of truth for "is the Agents Manager app loaded on this
|
||||
* request?". Used both to enqueue the app and to gate the server-side
|
||||
* sidebar pre-render, so the pre-rendered shell can never appear on a page
|
||||
* where the app won't mount to reconcile it.
|
||||
*
|
||||
* @return string|null The variant name, or null if scripts should not be loaded.
|
||||
*/
|
||||
public static function get_active_variant() {
|
||||
/**
|
||||
* Filter the script variant the Agents Manager loads for this request.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param string|null $variant The resolved variant, or null to not load.
|
||||
*/
|
||||
return apply_filters( 'agents_manager_variant', self::get_variant() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which script variant to load, or null if none should be loaded.
|
||||
*
|
||||
* Combines the gating logic (should we load at all?) with variant selection
|
||||
* (which build to use?) into a single method so the two cannot get out of sync.
|
||||
*
|
||||
* @return string|null The variant name, or null if scripts should not be loaded.
|
||||
*/
|
||||
private static function get_variant() {
|
||||
// CIAB: Load either the connected or disconnected variants if enabled.
|
||||
if ( self::is_ciab_environment() && self::is_enabled() ) {
|
||||
return self::is_jetpack_disconnected() ? 'ciab-disconnected' : 'ciab';
|
||||
}
|
||||
|
||||
// Frontend: load disconnected variant for eligible logged-in editors.
|
||||
if ( ! is_admin() ) {
|
||||
if ( self::is_loading_on_frontend() && self::is_enabled() ) {
|
||||
return 'wp-admin-disconnected';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Apply wp-admin exclusions (WooCommerce, customizer, preview contexts).
|
||||
if ( ! self::passes_admin_checks() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! self::is_enabled() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$disconnected = self::is_jetpack_disconnected();
|
||||
|
||||
if ( self::is_block_editor() ) {
|
||||
return $disconnected ? 'gutenberg-disconnected' : 'gutenberg';
|
||||
}
|
||||
|
||||
return $disconnected ? 'wp-admin-disconnected' : 'wp-admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the unified experience — the Help Center takeover — is active.
|
||||
*
|
||||
* "Unified" here means Agents Manager takes over the Help Center, unifying Odie and
|
||||
* Dolly (the orchestrator) into a single chat experience. This is distinct from
|
||||
* block-editor-only enablement, which replaces Big Sky's native UI without taking
|
||||
* over the Help Center.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_unified_experience() {
|
||||
/**
|
||||
* Filter to determine if the user should see the unified chat experience.
|
||||
*
|
||||
* When true, Help Center will render UnifiedAIAgent instead of traditional UI.
|
||||
* The filter is hooked by should_use_unified_experience() in this class.
|
||||
*
|
||||
* @param bool $use_unified_experience Whether to use unified experience. Default false.
|
||||
*/
|
||||
return (bool) apply_filters( 'agents_manager_use_unified_experience', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Agents Manager should be loaded in the current context.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
// CIAB: Agents Manager is the default AI experience — enabled unless explicitly
|
||||
// disabled via filter (e.g. for debugging or gradual rollout).
|
||||
if ( self::is_ciab_environment() ) {
|
||||
/**
|
||||
* Filter whether Agents Manager is enabled in CIAB (Next Admin) environments.
|
||||
*
|
||||
* @param bool $enabled Whether Agents Manager should load. Default true.
|
||||
*/
|
||||
return apply_filters( 'agents_manager_enabled_in_ciab', true );
|
||||
}
|
||||
|
||||
// Full unified experience: Agents Manager with support guides, Help Center takeover, etc.
|
||||
if ( self::is_unified_experience() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Block editor only: Agents Manager replaces Big Sky's native UI. Hooked by Big Sky.
|
||||
if ( self::is_block_editor() && apply_filters( 'agents_manager_enabled_in_block_editor', false ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current wp-admin context passes all exclusion checks.
|
||||
*
|
||||
* Excludes WooCommerce Admin home, customizer preview, Gutenberg asset requests,
|
||||
* and preview query param contexts.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function passes_admin_checks() {
|
||||
// Don't load on WooCommerce Admin home page to avoid UI conflicts.
|
||||
global $current_screen;
|
||||
if ( $current_screen && $current_screen->id === 'woocommerce_page_wc-admin' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't load in customizer preview iframe.
|
||||
if ( is_customize_preview() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't load during Gutenberg asset requests or preview contexts.
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
|
||||
$is_preview = isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) );
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
|
||||
$is_preview_overlay = isset( $_GET['preview_overlay'] );
|
||||
if ( str_contains( $request_uri, 'wp-content/plugins/gutenberg-core' ) || $is_preview || $is_preview_overlay ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Agents Manager script based on context.
|
||||
*
|
||||
* @param string $variant The variant of the asset file to get.
|
||||
*/
|
||||
private function enqueue_script( $variant ) {
|
||||
$cache_key = 'agents-manager-asset-' . $variant . '.asset.json';
|
||||
$asset_file = get_transient( $cache_key );
|
||||
|
||||
if ( ! $asset_file ) {
|
||||
$asset_file = self::get_assets_json( 'widgets.wp.com/agents-manager/agents-manager-' . $variant . '.asset.json' );
|
||||
if ( ! $asset_file ) {
|
||||
return;
|
||||
}
|
||||
set_transient( $cache_key, $asset_file, HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
// When the request is dev mode, use a random cache buster as the version for easier debugging.
|
||||
$version = self::is_dev_mode() ? wp_rand() : $asset_file['version'];
|
||||
|
||||
$script_dependencies = $asset_file['dependencies'] ?? array();
|
||||
|
||||
wp_enqueue_script(
|
||||
'agents-manager',
|
||||
'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . '.min.js',
|
||||
$script_dependencies,
|
||||
$version,
|
||||
/**
|
||||
* Filter the strategy to use when enqueuing the script.
|
||||
*
|
||||
* @param array|bool $args The arguments to pass to wp_enqueue_script. Default is true.
|
||||
* @param string $handle The handle of the script.
|
||||
*/
|
||||
apply_filters( 'agents_manager_enqueue_script_strategy', true, 'agents-manager' )
|
||||
);
|
||||
|
||||
if ( 'gutenberg-disconnected' !== $variant && 'ciab-disconnected' !== $variant ) {
|
||||
wp_enqueue_style(
|
||||
'agents-manager-style',
|
||||
'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . ( is_rtl() ? '.rtl.css' : '.css' ),
|
||||
array(),
|
||||
$version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the asset via file-system on wpcom and via network on Atomic sites.
|
||||
*
|
||||
* @param string $filepath The URL to download the asset file from.
|
||||
* @return array|null The asset file data or null on failure.
|
||||
*/
|
||||
private static function get_assets_json( $filepath ) {
|
||||
$accessible_directly = file_exists( ABSPATH . $filepath );
|
||||
|
||||
if ( $accessible_directly ) {
|
||||
$file_contents = file_get_contents( ABSPATH . $filepath );
|
||||
|
||||
if ( false === $file_contents ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode( $file_contents, true );
|
||||
}
|
||||
|
||||
$request = wp_remote_get( 'https://' . $filepath );
|
||||
|
||||
if ( is_wp_error( $request ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $request );
|
||||
if ( 200 !== $response_code ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content_type = wp_remote_retrieve_header( $request, 'content-type' );
|
||||
if ( is_string( $content_type ) && false === strpos( $content_type, 'json' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $request );
|
||||
if ( '' === $body ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode( $body, true );
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the calypso preferences.
|
||||
*
|
||||
* @param \stdClass $preferences The preferences.
|
||||
*
|
||||
* @return \stdClass The preferences.
|
||||
*/
|
||||
public function calypso_preferences_update( $preferences ) {
|
||||
// Check if agents_manager_router_history exists and is a valid array structure
|
||||
if ( ! isset( $preferences->agents_manager_router_history ) ||
|
||||
! is_array( $preferences->agents_manager_router_history ) ) {
|
||||
return $preferences;
|
||||
}
|
||||
|
||||
$router_history = $preferences->agents_manager_router_history;
|
||||
|
||||
// Check if entries exist and is an array
|
||||
if ( ! isset( $router_history['entries'] ) ||
|
||||
! is_array( $router_history['entries'] ) ) {
|
||||
return $preferences;
|
||||
}
|
||||
|
||||
$entries = $router_history['entries'];
|
||||
|
||||
// Limit entries to 50 to prevent spamming entries in the router history.
|
||||
if ( count( $entries ) > 50 ) {
|
||||
// Keep only the last 49 entries and add the root entry at the beginning.
|
||||
$entries = array_slice( $entries, -49 );
|
||||
// Keep the start at root so the back button always works.
|
||||
array_unshift(
|
||||
$entries,
|
||||
array(
|
||||
'pathname' => '/',
|
||||
'search' => '',
|
||||
'hash' => '',
|
||||
'key' => 'default',
|
||||
'state' => null,
|
||||
)
|
||||
);
|
||||
|
||||
// Update the preferences object directly
|
||||
$preferences->agents_manager_router_history['entries'] = $entries;
|
||||
$preferences->agents_manager_router_history['index'] = 49;
|
||||
}
|
||||
|
||||
return $preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates instance.
|
||||
*
|
||||
* @return Agents_Manager
|
||||
*/
|
||||
public static function init() {
|
||||
if ( did_action( 'jetpack_agents_manager_initialized' ) ) {
|
||||
return self::get_instance();
|
||||
}
|
||||
|
||||
self::$instance = new self();
|
||||
|
||||
/**
|
||||
* Fires once the Agents Manager class has been instantiated.
|
||||
*
|
||||
* @since 0.5.0
|
||||
*/
|
||||
do_action( 'jetpack_agents_manager_initialized' );
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the Agents Manager class.
|
||||
*
|
||||
* @return Agents_Manager
|
||||
*/
|
||||
public static function get_instance() {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current request is coming from the A8C proxy.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_proxied() {
|
||||
// On Simple sites, use the wpcom function if available.
|
||||
if ( function_exists( 'wpcom_is_proxied_request' ) ) {
|
||||
return wpcom_is_proxied_request();
|
||||
}
|
||||
|
||||
// On WoA/Garden sites, check server variable or constant.
|
||||
return isset( $_SERVER['A8C_PROXIED_REQUEST'] )
|
||||
? (bool) sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) )
|
||||
: Constants::is_true( 'A8C_PROXIED_REQUEST' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables "Development" features that should be accessible only for admins.
|
||||
*/
|
||||
private static function is_dev_mode() {
|
||||
// Known local environments.
|
||||
$domain = wp_parse_url( get_site_url(), PHP_URL_HOST );
|
||||
if (
|
||||
$domain === 'localhost' ||
|
||||
'.jurassic.tube' === stristr( $domain, '.jurassic.tube' ) ||
|
||||
'.jurassic.ninja' === stristr( $domain, '.jurassic.ninja' )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A8C development.
|
||||
if ( self::is_proxied() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( Constants::is_true( 'AT_PROXIED_REQUEST' ) && Constants::is_defined( 'ATOMIC_CLIENT_ID' ) ) {
|
||||
switch ( Constants::get_constant( 'ATOMIC_CLIENT_ID' ) ) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3: // Pressable
|
||||
case 32:
|
||||
case 118: // Commerce garden client (ciab)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Agents Manager endpoints.
|
||||
*/
|
||||
public function register_rest_api() {
|
||||
( new WP_REST_Agents_Manager_Persisted_Open_State() )->register_rest_route();
|
||||
( new WP_REST_Jetpack_AI_JWT() )->register_rest_route();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if user should see unified experience.
|
||||
*
|
||||
* @param bool $use_unified_experience Whether to use unified experience.
|
||||
* @return bool
|
||||
*/
|
||||
public function should_use_unified_experience( $use_unified_experience = false ) {
|
||||
// Early return for non-proxied/dev mode requests.
|
||||
// This feature is currently only available to Automattic employees testing via proxy.
|
||||
if ( ! self::is_dev_mode() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$is_simple_site = ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_simple();
|
||||
if ( $is_simple_site ) {
|
||||
// On Simple sites, evaluate locally.
|
||||
// Check Automattician and opt-in setting.
|
||||
$is_automattician = function_exists( '\is_automattician' ) && \is_automattician( $user_id );
|
||||
if ( $is_automattician && $this->has_unified_chat_opt_in_enabled( $user_id ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// On WoA and Garden sites, delegate to wpcom via the /agents-manager/state endpoint.
|
||||
// This avoids duplicating rollout logic and handles cases where
|
||||
// wpcom-specific functions (like get_user_attribute) aren't available.
|
||||
if ( $this->fetch_unified_experience_preference() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default to false, for now.
|
||||
// In the future: users with a big sky site (similar to https://github.a8c.com/Automattic/wpcom/pull/196449/files), a big-sky free trial or a paid plan.
|
||||
return $use_unified_experience;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has enabled unified chat opt-in in their Automattician options.
|
||||
*
|
||||
* This checks the unified_ai_chat calypso preference set via the wpcom profile settings.
|
||||
* Only used on Simple sites where get_user_attribute is available.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_unified_chat_opt_in_enabled( $user_id ) {
|
||||
if ( ! function_exists( '\get_user_attribute' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$calypso_prefs = \get_user_attribute( $user_id, 'calypso_preferences' );
|
||||
return ! empty( $calypso_prefs['unified_ai_chat'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch unified experience preference from wpcom via Jetpack Connection.
|
||||
*
|
||||
* Used on Atomic sites to delegate the decision to wpcom, which has
|
||||
* access to user attributes and can evaluate the rollout logic.
|
||||
*
|
||||
* Calls /agents-manager/state endpoint which is accessible via Jetpack user tokens.
|
||||
*
|
||||
* @return bool Whether user should see unified experience.
|
||||
*/
|
||||
private function fetch_unified_experience_preference() {
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check transient cache first (per-user cache).
|
||||
$cache_key = 'unified-experience-' . $user_id;
|
||||
$cached_result = get_transient( $cache_key );
|
||||
if ( false !== $cached_result ) {
|
||||
return (bool) $cached_result;
|
||||
}
|
||||
|
||||
// Check if user is connected before making API call.
|
||||
if ( ! ( new Connection_Manager() )->is_user_connected( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Call dedicated agents-manager/state endpoint.
|
||||
$wpcom_request = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user(
|
||||
'/agents-manager/state?key=unified_ai_chat',
|
||||
'2',
|
||||
array( 'method' => 'GET' )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $wpcom_request ) ) {
|
||||
// Cache failures too to avoid hammering the API.
|
||||
set_transient( $cache_key, 0, MINUTE_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
|
||||
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
|
||||
if ( 200 !== $response_code ) {
|
||||
set_transient( $cache_key, 0, MINUTE_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $wpcom_request );
|
||||
$decoded_body = json_decode( $body, true );
|
||||
|
||||
// The response is { "unified_ai_chat": true/false } when using key param.
|
||||
$result = is_array( $decoded_body ) && ! empty( $decoded_body['unified_ai_chat'] );
|
||||
|
||||
// Cache for 1 minute.
|
||||
set_transient( $cache_key, $result ? 1 : 0, MINUTE_IN_SECONDS );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current request is on the frontend and the user can edit posts.
|
||||
*
|
||||
* Mirrors Help_Center::is_loading_on_frontend().
|
||||
*
|
||||
* @return bool True if loading on the frontend for an eligible user.
|
||||
*/
|
||||
private static function is_loading_on_frontend() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
|
||||
if ( isset( $_GET['na_site_preview'] ) || isset( $_GET['preview_overlay'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
|
||||
if ( isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$can_edit_posts = current_user_can( 'edit_posts' ) && is_user_member_of_blog();
|
||||
return ! is_admin() && ! self::is_block_editor() && $can_edit_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current screen is the block editor.
|
||||
*
|
||||
* @return bool True if the current screen is the block editor.
|
||||
*/
|
||||
private static function is_block_editor() {
|
||||
if ( ! function_exists( 'get_current_screen' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current_screen = get_current_screen();
|
||||
// The widgets screen has the block editor but no Gutenberg top bar.
|
||||
return $current_screen && $current_screen->is_block_editor() && $current_screen->id !== 'widgets';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when Gutenberg's "admin bar in editor" (omnibar) experiment is active.
|
||||
*
|
||||
* Mirrors Gutenberg core's gate in `lib/experimental/admin-bar-in-editor/load.php`, and fails
|
||||
* safe when `gutenberg_is_experiment_enabled()` is unavailable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_admin_bar_in_editor() {
|
||||
return self::is_block_editor()
|
||||
&& is_admin_bar_showing()
|
||||
&& function_exists( 'gutenberg_is_experiment_enabled' )
|
||||
// @phan-suppress-next-line PhanUndeclaredFunction -- Guarded by function_exists() above.
|
||||
&& \gutenberg_is_experiment_enabled( 'gutenberg-admin-bar-in-editor' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current environment is CIAB (Commerce in a Box) / Next Admin.
|
||||
*
|
||||
* Uses the same detection method as Help Center: checks if next_admin_init has fired.
|
||||
*
|
||||
* @return bool True if CIAB/Next Admin environment.
|
||||
*/
|
||||
private static function is_ciab_environment() {
|
||||
return (bool) did_action( 'next_admin_init' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current user is NOT connected through Jetpack.
|
||||
*
|
||||
* Mirrors the logic from Help_Center::is_jetpack_disconnected().
|
||||
*
|
||||
* @return bool True if the site uses Jetpack but the current user is not connected.
|
||||
*/
|
||||
private static function is_jetpack_disconnected() {
|
||||
$user_id = get_current_user_id();
|
||||
$blog_id = get_current_blog_id();
|
||||
|
||||
if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) {
|
||||
return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
|
||||
}
|
||||
|
||||
if ( true === apply_filters( 'is_jetpack_site', false, $blog_id ) ) {
|
||||
return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user data for the agents manager.
|
||||
*
|
||||
* Mirrors the user data structure from Help Center's helpCenterData.
|
||||
*
|
||||
* @return array|null User data array or null if not logged in.
|
||||
*/
|
||||
private function get_current_user_data() {
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user_data = get_userdata( $user_id );
|
||||
if ( ! $user_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user_email = $user_data->user_email;
|
||||
|
||||
// Use wpcom_get_avatar_url on Simple sites, fall back to get_avatar_url elsewhere.
|
||||
if ( function_exists( 'wpcom_get_avatar_url' ) ) {
|
||||
$avatar_url = wpcom_get_avatar_url( $user_email, 64, '', true )[0];
|
||||
} else {
|
||||
$avatar_url = get_avatar_url( $user_id );
|
||||
}
|
||||
|
||||
return array(
|
||||
'ID' => $user_id,
|
||||
'username' => $user_data->user_login,
|
||||
'display_name' => $user_data->display_name,
|
||||
'avatar_URL' => $avatar_url,
|
||||
'email' => $user_email,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current site data for the agents manager.
|
||||
*
|
||||
* Returns minimal site data needed by AgentsManager (ID and domain only).
|
||||
* Uses jetpack_options['id'] on Atomic sites for the wpcom blog ID.
|
||||
*
|
||||
* @return array Site data with ID and domain.
|
||||
*/
|
||||
private function get_current_site() {
|
||||
/*
|
||||
* Atomic sites have the WP.com blog ID stored as a Jetpack option.
|
||||
* This code deliberately doesn't use `Jetpack_Options::get_option`
|
||||
* so it works even when Jetpack has not been loaded.
|
||||
*/
|
||||
$jetpack_options = get_option( 'jetpack_options' );
|
||||
if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) {
|
||||
$site_id = (int) $jetpack_options['id'];
|
||||
} else {
|
||||
$site_id = get_current_blog_id();
|
||||
}
|
||||
|
||||
return array(
|
||||
'ID' => $site_id,
|
||||
'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
|
||||
);
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
/**
|
||||
* Open_State_Store file.
|
||||
*
|
||||
* @package automattic/jetpack-agents-manager
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Agents_Manager;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Status\Host;
|
||||
|
||||
/**
|
||||
* Reads and writes the Agents Manager open state.
|
||||
*
|
||||
* The state is a global, per-user wpcom preference behind the
|
||||
* `/agents-manager/state` endpoint. How the server reads it depends on the site:
|
||||
*
|
||||
* - wpcom Simple: the preference is local, so read `calypso_preferences` directly.
|
||||
* - WoA / self-hosted: the preference is remote, so reads/writes go through this
|
||||
* store's local REST route, which calls wpcom over the Jetpack Connection and
|
||||
* caches the result in a per-user transient. Latency-sensitive readers (the
|
||||
* server-side pre-render) use that transient to skip the round-trip.
|
||||
*/
|
||||
class Open_State_Store {
|
||||
|
||||
/**
|
||||
* Transient key prefix for the cached per-user open state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const TRANSIENT_PREFIX = 'agents_manager_open_state_';
|
||||
|
||||
/**
|
||||
* Default state values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public const DEFAULTS = array(
|
||||
'agents_manager_open' => false,
|
||||
'agents_manager_docked' => false,
|
||||
'agents_manager_minimized' => false,
|
||||
'agents_manager_floating_position' => 'right',
|
||||
'agents_manager_router_history' => null,
|
||||
'agents_manager_last_activity' => null,
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch the open state from wpcom and refresh the cache.
|
||||
*
|
||||
* @return array|\WP_Error Normalized state, or WP_Error when the request fails.
|
||||
*/
|
||||
public static function fetch() {
|
||||
$body = Client::wpcom_json_api_request_as_user(
|
||||
'/agents-manager/state',
|
||||
'2',
|
||||
array( 'method' => 'GET' )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $body ) ) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$response = json_decode( wp_remote_retrieve_body( $body ), true );
|
||||
$state = self::normalize( is_array( $response ) ? $response : array() );
|
||||
|
||||
self::cache( $state );
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the open state to wpcom and refresh the cache.
|
||||
*
|
||||
* @param array $state Partial state to update (subset of DEFAULTS keys).
|
||||
* @return array|\WP_Error Normalized state, or WP_Error when the request fails.
|
||||
*/
|
||||
public static function update( array $state ) {
|
||||
$body = Client::wpcom_json_api_request_as_user(
|
||||
'/agents-manager/state',
|
||||
'2',
|
||||
array( 'method' => 'POST' ),
|
||||
array( 'state' => $state )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $body ) ) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$response = json_decode( wp_remote_retrieve_body( $body ), true );
|
||||
|
||||
if ( ! is_array( $response ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_response',
|
||||
'Invalid response from WPCOM endpoint',
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
$normalized = self::normalize( $response );
|
||||
|
||||
self::cache( $normalized );
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current user's open state from the fastest local source.
|
||||
*
|
||||
* For latency-sensitive callers like the server-side pre-render: Simple sites
|
||||
* read `calypso_preferences` directly, everywhere else uses the cached
|
||||
* transient (see the class docblock). Returns null when nothing is known yet,
|
||||
* so callers can skip pre-rendering until the frontend sets the real state.
|
||||
*
|
||||
* @return array|null `{ agents_manager_open, agents_manager_docked }` or null.
|
||||
*/
|
||||
public static function get_cached() {
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simple sites have the preference locally, so read it directly (the
|
||||
// transient is never primed there).
|
||||
if ( ( new Host() )->is_wpcom_simple() && function_exists( '\get_user_attribute' ) ) {
|
||||
$calypso_prefs = \get_user_attribute( $user_id, 'calypso_preferences' );
|
||||
if ( ! is_array( $calypso_prefs ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'agents_manager_open' => (bool) ( $calypso_prefs['agents_manager_open'] ?? false ),
|
||||
'agents_manager_docked' => (bool) ( $calypso_prefs['agents_manager_docked'] ?? false ),
|
||||
);
|
||||
}
|
||||
|
||||
$cached = get_transient( self::cache_key( $user_id ) );
|
||||
|
||||
return is_array( $cached ) ? $cached : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a raw endpoint response into the full state shape.
|
||||
*
|
||||
* @param array $response Raw decoded response.
|
||||
* @return array Normalized state with all DEFAULTS keys present.
|
||||
*/
|
||||
private static function normalize( array $response ): array {
|
||||
return array(
|
||||
'agents_manager_open' => (bool) ( $response['agents_manager_open'] ?? self::DEFAULTS['agents_manager_open'] ),
|
||||
'agents_manager_docked' => (bool) ( $response['agents_manager_docked'] ?? self::DEFAULTS['agents_manager_docked'] ),
|
||||
'agents_manager_minimized' => (bool) ( $response['agents_manager_minimized'] ?? self::DEFAULTS['agents_manager_minimized'] ),
|
||||
'agents_manager_floating_position' => $response['agents_manager_floating_position'] ?? self::DEFAULTS['agents_manager_floating_position'],
|
||||
'agents_manager_router_history' => $response['agents_manager_router_history'] ?? self::DEFAULTS['agents_manager_router_history'],
|
||||
'agents_manager_last_activity' => $response['agents_manager_last_activity'] ?? self::DEFAULTS['agents_manager_last_activity'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the open/docked bits in a per-user transient.
|
||||
*
|
||||
* Only used on the remote (WoA / self-hosted) path — it's what get_cached()
|
||||
* reads there. Simple sites read `calypso_preferences` directly and skip this.
|
||||
*
|
||||
* @param array $state Normalized state.
|
||||
*/
|
||||
private static function cache( array $state ): void {
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter how long the cached open state lives.
|
||||
*
|
||||
* It's refreshed on every read/write through this store, so the TTL mainly
|
||||
* caps how long a value changed elsewhere (e.g. in Calypso) stays stale.
|
||||
*
|
||||
* @since 0.4.0
|
||||
*
|
||||
* @param int $ttl Cache lifetime in seconds.
|
||||
*/
|
||||
$ttl = (int) apply_filters( 'agents_manager_open_state_cache_ttl', WEEK_IN_SECONDS );
|
||||
|
||||
set_transient(
|
||||
self::cache_key( $user_id ),
|
||||
array(
|
||||
'agents_manager_open' => (bool) ( $state['agents_manager_open'] ?? false ),
|
||||
'agents_manager_docked' => (bool) ( $state['agents_manager_docked'] ?? false ),
|
||||
),
|
||||
$ttl
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the per-user transient key.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return string
|
||||
*/
|
||||
private static function cache_key( int $user_id ): string {
|
||||
return self::TRANSIENT_PREFIX . $user_id;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/**
|
||||
* Sidebar Open Preservation file.
|
||||
*
|
||||
* @package automattic/jetpack-agents-manager
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Agents_Manager;
|
||||
|
||||
/**
|
||||
* Preserves the Agents Manager sidebar-open body classes across full wp-admin
|
||||
* navigations so the next page load can pre-apply them server-side (avoiding a
|
||||
* flicker before the React app boots).
|
||||
*
|
||||
* The open state comes from Open_State_Store::get_cached(), and the pre-render only
|
||||
* runs when the Agents Manager app is actually loading on this request — so the
|
||||
* pre-rendered shell is always reconciled by the app that mounts to manage it,
|
||||
* never left orphaned.
|
||||
*
|
||||
* The server can know the persisted "open && docked" preference, but not the
|
||||
* live viewport — and the docked layout only fits above a width breakpoint and
|
||||
* when the admin menu fits vertically. Width is handled in CSS (a static media
|
||||
* query). Height cannot be: the threshold is the *measured* #adminmenu height,
|
||||
* which varies per page. So a tiny synchronous viewport-height gate is printed on
|
||||
* `in_admin_header` (which fires after #adminmenu is in the DOM, before the
|
||||
* content paints) to re-evaluate the real dock gate and strip the pre-rendered
|
||||
* classes when the chat will actually float — pre-paint, so there is no flash.
|
||||
*/
|
||||
class Sidebar_Open_Preservation {
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Sidebar_Open_Preservation
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Body class marking the docked sidebar shell.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const SIDEBAR_CONTAINER_CLASS = 'agents-manager-sidebar-container';
|
||||
|
||||
/**
|
||||
* Body class marking the sidebar as open.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const SIDEBAR_OPEN_CLASS = 'agents-manager-sidebar-container--sidebar-open';
|
||||
|
||||
/**
|
||||
* Creates instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar_Open_Preservation constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Run last so our class sits at the end of the `admin_body_class` list. Otherwise
|
||||
// a later filter could append its class without a leading space and glue it onto
|
||||
// ours, breaking the CSS selector that pre-opens the sidebar.
|
||||
add_filter( 'admin_body_class', array( $this, 'add_preopen_body_classes' ), PHP_INT_MAX );
|
||||
|
||||
// Reconcile the pre-rendered shell against the live viewport before the
|
||||
// page content paints. `in_admin_header` fires after #adminmenu is in the
|
||||
// DOM, so the script can measure it the same way the React app does.
|
||||
add_action( 'in_admin_header', array( $this, 'print_sidebar_docking_gate_script' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject pre-open assistant classes in initial admin body markup.
|
||||
*
|
||||
* @param string $classes Existing admin body classes.
|
||||
* @return string
|
||||
*/
|
||||
public function add_preopen_body_classes( string $classes ): string {
|
||||
if ( ! $this->should_pre_render_docked_shell() ) {
|
||||
return $classes;
|
||||
}
|
||||
|
||||
$body_classes_with_sidebar_classes = implode(
|
||||
' ',
|
||||
array_filter(
|
||||
array(
|
||||
$classes,
|
||||
self::SIDEBAR_CONTAINER_CLASS,
|
||||
self::SIDEBAR_OPEN_CLASS,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return ' ' . $body_classes_with_sidebar_classes . ' ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the synchronous sidebar-docking reconciliation script.
|
||||
*
|
||||
* Only emitted when the docked shell was pre-rendered. Because we optimistically
|
||||
* inject the docked sidebar body classes, this script reconciles the gates that
|
||||
* the React hook applies and removes those classes so the chat floats instead.
|
||||
*
|
||||
* The script lives in src/js/sidebar-docking-gate.js and is inlined (not
|
||||
* referenced via `src`) on purpose: it must run render-blocking before paint,
|
||||
* and a same- or cross-origin fetch would add latency to that blocking window.
|
||||
* Reading the bundled file and printing it inline keeps it a real, lintable JS
|
||||
* file with zero request cost.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function print_sidebar_docking_gate_script() {
|
||||
if ( ! $this->should_pre_render_docked_shell() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
|
||||
if ( empty( $wp_filesystem ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
WP_Filesystem();
|
||||
}
|
||||
|
||||
$script_path = __DIR__ . '/../build/sidebar-docking-gate.js';
|
||||
|
||||
if ( empty( $wp_filesystem ) || ! $wp_filesystem->exists( $script_path ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$script = $wp_filesystem->get_contents( $script_path );
|
||||
if ( ! is_string( $script ) || '' === $script ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_print_inline_script_tag( $script );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the docked-open shell should be pre-rendered on this request.
|
||||
*
|
||||
* True only when the app is loading (so the shell will be reconciled by the
|
||||
* app that mounts to manage it) and the cached state is both open and docked
|
||||
* — the only state that reshapes the admin layout. A cold session (no cache),
|
||||
* a closed sidebar, or a floating (undocked) chat all pre-render nothing.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_pre_render_docked_shell(): bool {
|
||||
if ( ! $this->should_preserve_sidebar_open_state() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$state = Open_State_Store::get_cached();
|
||||
|
||||
return $state && true === $state['agents_manager_open'] && true === $state['agents_manager_docked'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether sidebar open preservation should run for this request.
|
||||
*
|
||||
* Gated on the same decision that loads the app (its active variant), so the
|
||||
* pre-rendered shell only appears where the app will mount to reconcile it.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_preserve_sidebar_open_state(): bool {
|
||||
return null !== Agents_Manager::get_active_variant();
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_REST_Agents_Manager_Persisted_Open_State file.
|
||||
*
|
||||
* @package automattic/jetpack-agents-manager
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Agents_Manager;
|
||||
|
||||
/**
|
||||
* Class WP_REST_Agents_Manager_Persisted_Open_State.
|
||||
*/
|
||||
class WP_REST_Agents_Manager_Persisted_Open_State extends \WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* WP_REST_Agents_Manager_Persisted_Open_State constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'agents-manager';
|
||||
$this->rest_base = '/open-state';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register available routes.
|
||||
*/
|
||||
public function register_rest_route() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
array(
|
||||
// Get the open state.
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_state' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
),
|
||||
// Set the open state
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'set_state' ),
|
||||
'permission_callback' => 'is_user_logged_in',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Agents Manager state from user preferences.
|
||||
*
|
||||
* Delegates to Open_State_Store, which fetches the dedicated
|
||||
* wpcom/v2/agents-manager/state endpoint and refreshes the local cache.
|
||||
*/
|
||||
public function get_state() {
|
||||
$state = Open_State_Store::fetch();
|
||||
|
||||
if ( is_wp_error( $state ) ) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
return rest_ensure_response( $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Agents Manager state in user preferences.
|
||||
*
|
||||
* Delegates to Open_State_Store, which updates the dedicated
|
||||
* wpcom/v2/agents-manager/state endpoint and refreshes the local cache.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request sent to the API.
|
||||
*/
|
||||
public function set_state( \WP_REST_Request $request ) {
|
||||
$state = array();
|
||||
|
||||
foreach ( array_keys( Open_State_Store::DEFAULTS ) as $key ) {
|
||||
if ( $request->has_param( $key ) ) {
|
||||
$state[ $key ] = $request[ $key ];
|
||||
}
|
||||
}
|
||||
|
||||
$result = Open_State_Store::update( $state );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return rest_ensure_response( $result );
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* WP_REST_Jetpack_AI_JWT file.
|
||||
*
|
||||
* @package automattic/jetpack-agents-manager
|
||||
*/
|
||||
|
||||
namespace Automattic\Jetpack\Agents_Manager;
|
||||
|
||||
use Automattic\Jetpack\Connection\Client;
|
||||
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
|
||||
use Jetpack_Options;
|
||||
|
||||
/**
|
||||
* Class WP_REST_Jetpack_AI_JWT.
|
||||
*/
|
||||
class WP_REST_Jetpack_AI_JWT extends \WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* WP_REST_Jetpack_AI_JWT constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->namespace = 'jetpack/v4';
|
||||
$this->rest_base = '/jetpack-ai-jwt';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register available routes.
|
||||
*/
|
||||
public function register_rest_route() {
|
||||
/*
|
||||
* Check if the `jetpack/v4/jetpack-ai-jwt` endpoint is registered
|
||||
* by the Jetpack plugin to avoid registering it again.
|
||||
* In case it's not registered, register it
|
||||
* to make it available for Jetpack products that depend on it.
|
||||
*/
|
||||
if ( $this->is_rest_endpoint_registered() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
$this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'get_jwt' ),
|
||||
'permission_callback' => array( $this, 'permission_callback' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this REST endpoint is already registered.
|
||||
*
|
||||
* @return bool True if the endpoint is registered, false otherwise.
|
||||
*/
|
||||
private function is_rest_endpoint_registered() {
|
||||
$server = rest_get_server();
|
||||
$routes = $server->get_routes();
|
||||
$full_endpoint = '/' . trim( $this->namespace, '/' ) . $this->rest_base;
|
||||
|
||||
return isset( $routes[ $full_endpoint ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for the JWT endpoint.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function permission_callback() {
|
||||
return ( new Connection_Manager( 'jetpack' ) )->is_user_connected() && current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask WPCOM for a JWT token to use for OpenAI completion.
|
||||
*/
|
||||
public function get_jwt() {
|
||||
$blog_id = Jetpack_Options::get_option( 'id' );
|
||||
|
||||
$response = Client::wpcom_json_api_request_as_user(
|
||||
"/sites/$blog_id/jetpack-openai-query/jwt",
|
||||
'2',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
|
||||
),
|
||||
wp_json_encode( array(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
|
||||
'wpcom'
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$json = json_decode( wp_remote_retrieve_body( $response ) );
|
||||
|
||||
if ( ! isset( $json->token ) ) {
|
||||
return new \WP_Error(
|
||||
'no-token',
|
||||
'No token returned from WPCOM',
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'token' => $json->token,
|
||||
'blog_id' => $blog_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Agents Manager pre-paint sidebar-docking gate.
|
||||
*
|
||||
* The assistant floats instead of being docked in certain conditions. Because we
|
||||
* optimistically inject the sidebar classes, we remove them when those conditions
|
||||
* are met.
|
||||
*
|
||||
* IMPORTANT: Keep this logic in sync with
|
||||
* `calypso/packages/agents-manager/src/hooks/use-agent-layout-manager/index.tsx`.
|
||||
*/
|
||||
|
||||
const FULLSCREEN_GATED_BODY_CLASSES = [ 'post-php', 'post-new-php', 'site-editor-php' ];
|
||||
const FULLSCREEN_BODY_CLASS = 'is-fullscreen-mode';
|
||||
|
||||
const DOCKED_SIDEBAR_BODY_CLASSES = [
|
||||
'agents-manager-sidebar-container',
|
||||
'agents-manager-sidebar-container--sidebar-open',
|
||||
];
|
||||
|
||||
// The Calypso hook adds this element when it mounts; its presence is our hand-off signal.
|
||||
const CHAT_PORTAL_CLASS = 'agents-manager-chat';
|
||||
|
||||
// Minimum viewport width for docking, mirroring the hook's `desktopMediaQuery`
|
||||
// default of `(min-width: 1200px)`.
|
||||
const DESKTOP_MIN_WIDTH = 1200;
|
||||
|
||||
/**
|
||||
* Whether the fullscreen gate is open.
|
||||
*
|
||||
* @return {boolean} Whether the fullscreen gate is open.
|
||||
*/
|
||||
function isFullscreenGateOpen() {
|
||||
const { classList } = document.body;
|
||||
const isGated = FULLSCREEN_GATED_BODY_CLASSES.some( cls => classList.contains( cls ) );
|
||||
return ! isGated || classList.contains( FULLSCREEN_BODY_CLASS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the docked shell is currently on the body.
|
||||
*
|
||||
* @return {boolean} Whether the docked shell is present.
|
||||
*/
|
||||
function isDockedShellPresent() {
|
||||
return DOCKED_SIDEBAR_BODY_CLASSES.some( cls => document.body.classList.contains( cls ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the docked shell so the assistant floats.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
function removeDockedShell() {
|
||||
document.body.classList.remove( ...DOCKED_SIDEBAR_BODY_CLASSES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the sidebar docking gate evaluation.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
function runSidebarDockingGate() {
|
||||
if ( ! document.body ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adminMenu = document.getElementById( 'adminmenu' );
|
||||
if ( ! adminMenu ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The docked layout pins the admin menu to the viewport; if the menu is taller
|
||||
// than the room below the admin bar it would be clipped, so the chat floats instead.
|
||||
const adminBar = document.getElementById( 'wpadminbar' );
|
||||
const adminBarHeight = adminBar ? adminBar.offsetHeight : 32;
|
||||
const tooShort = window.innerHeight < adminMenu.offsetHeight + adminBarHeight + 20;
|
||||
const tooNarrow = window.innerWidth < DESKTOP_MIN_WIDTH;
|
||||
|
||||
if ( tooShort || tooNarrow || ! isFullscreenGateOpen() ) {
|
||||
removeDockedShell();
|
||||
}
|
||||
}
|
||||
|
||||
runSidebarDockingGate();
|
||||
|
||||
// The gated screen classes (`post-php`, …) and `is-fullscreen-mode` can land on
|
||||
// <body> after the first run, leaving a stale shell on a non-fullscreen editor. So
|
||||
// re-check the fullscreen gate on class changes (cheap reads, no layout) and float
|
||||
// the chat if it closed. Disconnect once the shell is gone (which also stops a
|
||||
// re-trigger loop) or the app mounts and takes over docking.
|
||||
if ( document.body ) {
|
||||
const observer = new MutationObserver( () => {
|
||||
if ( ! isDockedShellPresent() || document.querySelector( `.${ CHAT_PORTAL_CLASS }` ) ) {
|
||||
observer.disconnect();
|
||||
return;
|
||||
}
|
||||
if ( ! isFullscreenGateOpen() ) {
|
||||
removeDockedShell();
|
||||
}
|
||||
} );
|
||||
observer.observe( document.body, { attributes: true, attributeFilter: [ 'class' ] } );
|
||||
}
|
||||
Reference in New Issue
Block a user