Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.67% covered (success)
94.67%
426 / 450
71.43% covered (warning)
71.43%
25 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
Agents_Manager
94.67% covered (success)
94.67%
426 / 450
71.43% covered (warning)
71.43%
25 / 35
167.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 should_display_menu_panel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_icon
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 add_help_menu
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 add_menu_panel
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
1 / 1
1
 add_ai_chat_button
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 get_active_context
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 add_admin_bar_nodes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
15
 enqueue_scripts
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
7
 get_active_variant
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_plugin_information_iframe
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 get_variant
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
12
 is_unified_experience
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_enabled
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 passes_admin_checks
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 enqueue_script
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
9
 determine_iso_639_locale
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 get_assets_json
63.64% covered (warning)
63.64%
14 / 22
0.00% covered (danger)
0.00%
0 / 1
12.89
 calypso_preferences_update
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 get_instance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_proxied
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 is_tracking_automattician
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 is_dev_mode
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
12
 register_rest_api
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 should_use_unified_experience
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 has_unified_chat_opt_in_enabled
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 fetch_unified_experience_preference
88.46% covered (warning)
88.46%
23 / 26
0.00% covered (danger)
0.00%
0 / 1
8.10
 is_loading_on_frontend
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
10.37
 is_block_editor
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 is_admin_bar_in_editor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_ciab_environment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_jetpack_disconnected
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 get_current_user_data
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
4.03
 get_current_site
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Agents manager
4 *
5 * @package automattic/jetpack-agents-manager
6 */
7
8namespace Automattic\Jetpack\Agents_Manager;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Connection\REST_Jetpack_AI_JWT;
12use Automattic\Jetpack\Constants;
13
14/**
15 * Class Agents_Manager
16 */
17class Agents_Manager {
18    /**
19     * The package version of the Agents Manager package.
20     *
21     * @var string
22     */
23    const PACKAGE_VERSION = '0.12.0';
24
25    /**
26     * Help Center URL for disconnected variants.
27     *
28     * @var string
29     */
30    private const HELP_CENTER_URL = 'https://wordpress.com/help?help-center=home';
31
32    /**
33     * Class instance.
34     *
35     * @var Agents_Manager
36     */
37    private static $instance = null;
38
39    /**
40     * Agents_Manager constructor.
41     */
42    private function __construct() {
43        add_action( 'rest_api_init', array( $this, 'register_rest_api' ) );
44        add_filter( 'calypso_preferences_update', array( $this, 'calypso_preferences_update' ) );
45
46        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
47        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 101 );
48        add_action( 'next_admin_init', array( $this, 'enqueue_scripts' ), 1001 );
49
50        // Runs after Help Center's own admin_bar_menu callback, so add_help_menu() can replace its node.
51        add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_nodes' ), 100 );
52
53        add_filter( 'agents_manager_use_unified_experience', array( $this, 'should_use_unified_experience' ) );
54
55        Sidebar_Open_Preservation::init();
56    }
57
58    /**
59     * Check if the agents manager menu panel should be displayed.
60     *
61     * @return bool True if the menu panel should be displayed.
62     */
63    public function should_display_menu_panel() {
64        return self::is_unified_experience();
65    }
66
67    /**
68     * Get the SVG icon markup for a given icon name.
69     *
70     * @param string $icon_name The name of the icon to retrieve.
71     * @return string The SVG markup.
72     */
73    private function get_icon( $icon_name ) {
74        // Not cached: the sparkle icon's aria-label is translated, and the locale can switch mid-request.
75        $icons = array(
76            '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>',
77            '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>',
78            '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>',
79            '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>',
80            '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>',
81            'help'    => '<svg id="agents-manager-icon" class="ab-icon" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
82                            <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" />
83                        </svg>',
84            'sparkle' => '<svg class="ab-icon" role="img" aria-label="' . esc_attr__( 'Agent', 'jetpack-agents-manager' ) . '" width="24" height="24" viewBox="-45 -45 490 490" xmlns="http://www.w3.org/2000/svg">
85                                <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" />
86                            </svg>',
87        );
88
89        return $icons[ $icon_name ] ?? '';
90    }
91
92    /**
93     * Add the Agents Manager Help "?" node (`agents-manager`) to the admin bar, replacing the
94     * legacy Help Center node (`help-center`).
95     *
96     * @param \WP_Admin_Bar $wp_admin_bar     The WP_Admin_Bar instance.
97     * @param bool          $use_disconnected Disconnected variants link straight to the Help Center instead of opening the dropdown.
98     */
99    public function add_help_menu( $wp_admin_bar, $use_disconnected ) {
100        $wp_admin_bar->remove_node( 'help-center' );
101
102        $menu_args = array(
103            'id'     => 'agents-manager',
104            'title'  => '<span title="' . esc_attr__( 'Help Center', 'jetpack-agents-manager' ) . '">' . $this->get_icon( 'help' ) . '</span>',
105            'parent' => 'top-secondary',
106        );
107
108        if ( $use_disconnected ) {
109            $menu_args['href'] = self::HELP_CENTER_URL;
110            $menu_args['meta'] = array(
111                'menu_title' => __( 'Help Center', 'jetpack-agents-manager' ),
112                'icon'       => 'help',
113                'target'     => '_blank',
114                'rel'        => 'noopener noreferrer',
115            );
116        } else {
117            $menu_args['meta'] = array(
118                'menu_title' => __( 'Help Center', 'jetpack-agents-manager' ),
119                'icon'       => 'help',
120                'class'      => 'menupop',
121            );
122        }
123
124        $wp_admin_bar->add_menu( $menu_args );
125    }
126
127    /**
128     * Add the agents manager menu panel to the admin bar.
129     *
130     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
131     */
132    public function add_menu_panel( $wp_admin_bar ) {
133        // Add chat support group
134        $wp_admin_bar->add_group(
135            array(
136                'parent' => 'agents-manager',
137                'id'     => 'agents-manager-menu-panel-chat',
138                'meta'   => array(
139                    'class' => 'ab-sub-secondary',
140                ),
141            )
142        );
143
144        // Add chat support menu item
145        $wp_admin_bar->add_node(
146            array(
147                'parent' => 'agents-manager-menu-panel-chat',
148                'id'     => 'agents-manager-chat-support',
149                'title'  => $this->get_icon( 'comment' ) . '<span>' . __( 'Chat support', 'jetpack-agents-manager' ) . '</span>',
150                'meta'   => array(
151                    'menu_title' => __( 'Chat support', 'jetpack-agents-manager' ),
152                    'icon'       => 'comment',
153                    'route'      => '/chat',
154                ),
155            )
156        );
157
158        // Add chat history menu item
159        $wp_admin_bar->add_node(
160            array(
161                'parent' => 'agents-manager-menu-panel-chat',
162                'id'     => 'agents-manager-chat-history',
163                'title'  => $this->get_icon( 'backup' ) . '<span>' . __( 'Chat history', 'jetpack-agents-manager' ) . '</span>',
164                'meta'   => array(
165                    'menu_title' => __( 'Chat history', 'jetpack-agents-manager' ),
166                    'icon'       => 'backup',
167                    'route'      => '/history',
168                ),
169            )
170        );
171
172        // Add links group
173        $wp_admin_bar->add_group(
174            array(
175                'parent' => 'agents-manager',
176                'id'     => 'agents-manager-menu-panel-links',
177                'meta'   => array(
178                    'class' => 'ab-sub-secondary',
179                ),
180            )
181        );
182
183        // Add support guides menu item
184        $wp_admin_bar->add_node(
185            array(
186                'parent' => 'agents-manager-menu-panel-links',
187                'id'     => 'agents-manager-support-guides',
188                'title'  => $this->get_icon( 'page' ) . '<span>' . __( 'Support guides', 'jetpack-agents-manager' ) . '</span>',
189                'meta'   => array(
190                    'menu_title' => __( 'Support guides', 'jetpack-agents-manager' ),
191                    'icon'       => 'page',
192                    'route'      => '/support-guides',
193                ),
194            )
195        );
196
197        // Add courses menu item
198        $wp_admin_bar->add_node(
199            array(
200                'parent' => 'agents-manager-menu-panel-links',
201                'id'     => 'agents-manager-courses',
202                'title'  => $this->get_icon( 'video' ) . '<span>' . __( 'Courses', 'jetpack-agents-manager' ) . '</span>',
203                'href'   => 'https://wordpress.com/support/courses/',
204                'meta'   => array(
205                    'menu_title' => __( 'Courses', 'jetpack-agents-manager' ),
206                    'icon'       => 'video',
207                    'target'     => '_blank',
208                    'rel'        => 'noopener noreferrer',
209                ),
210            )
211        );
212
213        // Add product updates menu item
214        $wp_admin_bar->add_node(
215            array(
216                'parent' => 'agents-manager-menu-panel-links',
217                'id'     => 'agents-manager-product-updates',
218                'title'  => $this->get_icon( 'rss' ) . '<span>' . __( 'Product updates', 'jetpack-agents-manager' ) . '</span>',
219                'href'   => 'https://wordpress.com/blog/category/product-features/',
220                'meta'   => array(
221                    'menu_title' => __( 'Product updates', 'jetpack-agents-manager' ),
222                    'icon'       => 'rss',
223                    'target'     => '_blank',
224                    'rel'        => 'noopener noreferrer',
225                ),
226            )
227        );
228    }
229
230    /**
231     * Add the standalone AI chat button to the admin bar.
232     *
233     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
234     */
235    public function add_ai_chat_button( $wp_admin_bar ) {
236        $meta = array(
237            'menu_title' => __( 'Agent', 'jetpack-agents-manager' ),
238            'icon'       => 'sparkle',
239            // The wp-admin bundle mounts the chat into this div.
240            'html'       => '<div id="agents-manager-masterbar"></div>',
241        );
242
243        // The "Agent" label shows while the chat is hidden (closed or minimized).
244        // Pre-hide it when the chat will restore visible; the bundle keeps the
245        // class in step afterwards.
246        $state = Open_State_Store::get_cached();
247        if ( ! empty( $state['agents_manager_open'] ) && empty( $state['agents_manager_minimized'] ) ) {
248            $meta['class'] = 'is-chat-visible';
249        }
250
251        $wp_admin_bar->add_menu(
252            array(
253                'id'     => 'agents-manager-ai-chat',
254                'parent' => 'top-secondary',
255                'title'  => '<span title="' . esc_attr__( 'Agent', 'jetpack-agents-manager' ) . '">' . $this->get_icon( 'sparkle' ) . '</span>'
256                    . '<span class="agents-manager-ai-chat-label" aria-hidden="true"><span>' . esc_html__( 'Agent', 'jetpack-agents-manager' ) . '</span></span>',
257                'meta'   => $meta,
258            )
259        );
260    }
261
262    /**
263     * The Agents Manager context for this request, or null when it should not load.
264     *
265     * Shared by `add_admin_bar_nodes()` and `enqueue_scripts()`.
266     *
267     * @return array{variant: string, disconnected: bool, gutenberg: bool, unified: bool, ciab: bool, enabled: bool}|null
268     */
269    private function get_active_context() {
270        // P2 frontends get neither the admin bar entry points nor the app.
271        $stylesheet = get_stylesheet();
272        $is_p2      = str_contains( $stylesheet, 'pub/p2' ) || function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( get_current_blog_id() );
273
274        if ( ! is_admin() && $is_p2 ) {
275            return null;
276        }
277
278        // Determine which variant to load (null = don't load).
279        $variant = self::get_active_variant();
280        if ( null === $variant ) {
281            return null;
282        }
283
284        return array(
285            'variant'      => $variant,
286            'disconnected' => str_contains( $variant, 'disconnected' ),
287            'gutenberg'    => $this->is_block_editor(),
288            'unified'      => self::is_unified_experience(),
289            'ciab'         => $this->is_ciab_environment(),
290            'enabled'      => self::is_enabled(),
291        );
292    }
293
294    /**
295     * Add the Agents Manager entry points to the admin bar.
296     *
297     * Hooked unconditionally, with eligibility resolved here rather than at registration, so the
298     * admin-bar REST endpoints â€” which fire `admin_bar_menu` with no enqueue hook â€” get the same
299     * nodes as a page load.
300     *
301     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
302     */
303    public function add_admin_bar_nodes( $wp_admin_bar ) {
304        $context = $this->get_active_context();
305        if ( null === $context ) {
306            return;
307        }
308
309        // For non-Gutenberg, non-CIAB environments, add to the admin bar. Gutenberg uses JS when
310        // no admin bar is visible and the server-side branch below when one is available. CIAB
311        // hides the admin bar and uses its own Site Hub.
312        if ( ! $context['gutenberg'] && ! $context['ciab'] ) {
313            // Disconnected variants are Help-only. Full variants take over Help
314            // only when the unified experience is active.
315            if ( $context['disconnected'] || $context['unified'] ) {
316                $this->add_help_menu( $wp_admin_bar, $context['disconnected'] );
317            }
318
319            // Initialize the Help menu panel only for the full unified experience.
320            if ( ! $context['disconnected'] && $context['unified'] ) {
321                $this->add_menu_panel( $wp_admin_bar );
322            }
323
324            // Standalone AI chat button, shown whenever the full Agents Manager app is enabled.
325            if ( ! $context['disconnected'] && $context['enabled'] ) {
326                $this->add_ai_chat_button( $wp_admin_bar );
327            }
328        }
329
330        // When the block editor exposes the WordPress admin bar, add the entry points there.
331        // CIAB is excluded â€” it has its own Site Hub UI.
332        // Mirroring wp-admin: the Help "?" dropdown shows only in the full unified experience;
333        // the AI chat button shows whenever Agents Manager is enabled in this editor.
334        if ( ! $context['ciab'] && ! $context['disconnected'] && self::is_admin_bar_in_editor() ) {
335            // Help "?" node + dropdown panel first, matching the wp-admin admin bar order.
336            if ( $context['unified'] ) {
337                $this->add_help_menu( $wp_admin_bar, false );
338                $this->add_menu_panel( $wp_admin_bar );
339            }
340
341            if ( $context['enabled'] ) {
342                $this->add_ai_chat_button( $wp_admin_bar );
343            }
344        }
345    }
346
347    /**
348     * Enqueue Agents Manager scripts and add inline script data.
349     */
350    public function enqueue_scripts() {
351        $context = $this->get_active_context();
352        if ( null === $context ) {
353            return;
354        }
355
356        $variant = $context['variant'];
357
358        // In Gutenberg, dequeue Help Center so we don't end up with two buttons â€” but only
359        // in the full unified experience, where Agents Manager takes over the Help Center.
360        // In block-editor-only mode (e.g. ?flags=unified-big-sky) Agents Manager replaces
361        // Big Sky's native UI and Help Center should remain available.
362        // Agents Manager fires at priority 101, after Help Center at 100, so HC is already enqueued.
363        if ( $context['gutenberg'] && $context['unified'] ) {
364            wp_dequeue_script( 'help-center' );
365            wp_dequeue_style( 'help-center-style' );
366        }
367
368        /**
369         * Filter to register agent provider modules for the Agents Manager.
370         *
371         * Plugins can hook into this filter to register script module IDs that export
372         * toolProvider and/or contextProvider. The Agents Manager JS will dynamically
373         * import these modules and merge their providers.
374         *
375         * @param array $providers Array of provider script module IDs.
376         */
377        $agent_providers = apply_filters( 'agents_manager_agent_providers', array() );
378
379        /**
380         * Filter the default agent ID for the Agents Manager.
381         *
382         * Allows host applications (e.g., CIAB, WooCommerce AI) to specify a custom
383         * workflow agent instead of the default orchestrator. The value is passed to
384         * the frontend as `agentsManagerData.agentId` and consumed by `useAgentConfig()`.
385         *
386         * @param string|null $agent_id The agent ID to use, or null for default behavior.
387         */
388        $agent_id = apply_filters( 'agents_manager_agent_id', null );
389
390        $script_version = $this->enqueue_script( $variant );
391
392        $inline_data = array(
393            'agentProviders'       => $agent_providers,
394            'useUnifiedExperience' => $context['unified'],
395            'isDevMode'            => self::is_dev_mode(),
396            'isA11n'               => self::is_tracking_automattician(),
397            'isWpcomPlatform'      => ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_platform(),
398            'sectionName'          => apply_filters( 'agents_manager_section_name', $variant ),
399            'currentUser'          => $this->get_current_user_data(),
400            'site'                 => $this->get_current_site(),
401            'helpCenterUrl'        => self::HELP_CENTER_URL,
402        );
403
404        if ( null !== $script_version ) {
405            $inline_data['version'] = $variant . ':' . $script_version;
406        }
407
408        if ( $agent_id ) {
409            $inline_data['agentId'] = $agent_id;
410        }
411
412        /**
413         * Filter the data exposed to the Agents Manager frontend.
414         *
415         * @param array $inline_data Data encoded into `agentsManagerData`.
416         */
417        $filtered    = apply_filters( 'jetpack_ai_sidebar_agents_manager_data', $inline_data );
418        $inline_data = is_array( $filtered ) ? $filtered : $inline_data;
419
420        wp_add_inline_script(
421            'agents-manager',
422            'const agentsManagerData = ' . wp_json_encode(
423                $inline_data,
424                JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
425            ) . ';',
426            'before'
427        );
428    }
429
430    /**
431     * The script variant active for this request, or null if none.
432     *
433     * Single source of truth for "is the Agents Manager app loaded on this
434     * request?". Used both to enqueue the app and to gate the server-side
435     * sidebar pre-render, so the pre-rendered shell can never appear on a page
436     * where the app won't mount to reconcile it.
437     *
438     * @return string|null The variant name, or null if scripts should not be loaded.
439     */
440    public static function get_active_variant() {
441        if ( self::is_plugin_information_iframe() ) {
442            return null;
443        }
444
445        /**
446         * Filter the script variant the Agents Manager loads for this request.
447         *
448         * @since 0.1.0
449         *
450         * @param string|null $variant The resolved variant, or null to not load.
451         */
452        return apply_filters( 'agents_manager_variant', self::get_variant() );
453    }
454
455    /**
456     * Whether the current request renders the plugin information iframe.
457     *
458     * The parent plugin screen may load Agents Manager, but the iframe must not
459     * bootstrap a second copy of the app.
460     *
461     * @return bool
462     */
463    private static function is_plugin_information_iframe() {
464        global $current_screen;
465
466        if ( ! $current_screen || 'plugin-install' !== $current_screen->id ) {
467            return false;
468        }
469
470        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a request context check, not a form submission.
471        return isset( $_GET['tab'] ) && 'plugin-information' === sanitize_text_field( wp_unslash( $_GET['tab'] ) );
472    }
473
474    /**
475     * Determine which script variant to load, or null if none should be loaded.
476     *
477     * Combines the gating logic (should we load at all?) with variant selection
478     * (which build to use?) into a single method so the two cannot get out of sync.
479     *
480     * @return string|null The variant name, or null if scripts should not be loaded.
481     */
482    private static function get_variant() {
483        // CIAB: Load either the connected or disconnected variants if enabled.
484        if ( self::is_ciab_environment() && self::is_enabled() ) {
485            return self::is_jetpack_disconnected() ? 'ciab-disconnected' : 'ciab';
486        }
487
488        // Frontend: load disconnected variant for eligible logged-in editors.
489        if ( ! is_admin() ) {
490            if ( self::is_loading_on_frontend() && self::is_enabled() ) {
491                return 'wp-admin-disconnected';
492            }
493            return null;
494        }
495
496        // Apply wp-admin exclusions (customizer, asset, and preview contexts).
497        if ( ! self::passes_admin_checks() ) {
498            return null;
499        }
500
501        if ( ! self::is_enabled() ) {
502            return null;
503        }
504
505        $disconnected = self::is_jetpack_disconnected();
506
507        if ( self::is_block_editor() ) {
508            return $disconnected ? 'gutenberg-disconnected' : 'gutenberg';
509        }
510
511        return $disconnected ? 'wp-admin-disconnected' : 'wp-admin';
512    }
513
514    /**
515     * Whether the unified experience â€” the Help Center takeover â€” is active.
516     *
517     * "Unified" here means Agents Manager takes over the Help Center, unifying Odie and
518     * Dolly (the orchestrator) into a single chat experience. This is distinct from
519     * block-editor-only enablement, which replaces Big Sky's native UI without taking
520     * over the Help Center.
521     *
522     * @return bool
523     */
524    public static function is_unified_experience() {
525        /**
526         * Filter to determine if the user should see the unified chat experience.
527         *
528         * When true, Help Center will render UnifiedAIAgent instead of traditional UI.
529         * The filter is hooked by should_use_unified_experience() in this class.
530         *
531         * @param bool $use_unified_experience Whether to use unified experience. Default false.
532         */
533        return (bool) apply_filters( 'agents_manager_use_unified_experience', false );
534    }
535
536    /**
537     * Returns true if the Agents Manager should be loaded in the current context.
538     *
539     * @return bool
540     */
541    public static function is_enabled() {
542        $enabled = false;
543
544        // CIAB: Agents Manager is the default AI experience â€” enabled unless explicitly
545        // disabled via filter (e.g. for debugging or gradual rollout).
546        if ( self::is_ciab_environment() ) {
547            /**
548             * Filter whether Agents Manager is enabled in CIAB (Next Admin) environments.
549             *
550             * @param bool $enabled Whether Agents Manager should load. Default true.
551             */
552            $enabled = (bool) apply_filters( 'agents_manager_enabled_in_ciab', true );
553        } elseif ( self::is_unified_experience() ) {
554            // Full unified experience: Agents Manager with support guides, Help Center takeover, etc.
555            $enabled = true;
556        } elseif ( self::is_block_editor() && apply_filters( 'agents_manager_enabled_in_block_editor', false ) ) {
557            // Block editor only: Agents Manager replaces Big Sky's native UI. Hooked by Big Sky.
558            $enabled = true;
559        }
560
561        /**
562         * Filters whether an integration requests the Agents Manager shell on this request.
563         *
564         * Providers should preserve an existing true value so multiple integrations can
565         * request the shared shell independently.
566         *
567         * @since 0.9.1
568         *
569         * @param bool $should_load Whether another integration already requested the shell.
570         */
571        $should_load = (bool) apply_filters( 'agents_manager_should_load', false );
572
573        return $enabled || $should_load;
574    }
575
576    /**
577     * Returns true if the current wp-admin context passes all exclusion checks.
578     *
579     * Excludes customizer previews, Gutenberg asset requests, and preview query
580     * param contexts.
581     *
582     * @return bool
583     */
584    private static function passes_admin_checks() {
585        // Don't load in customizer preview iframe.
586        if ( is_customize_preview() ) {
587            return false;
588        }
589
590        // Don't load during Gutenberg asset requests or preview contexts.
591        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
592        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
593        $is_preview = isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) );
594        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
595        $is_preview_overlay = isset( $_GET['preview_overlay'] );
596        if ( str_contains( $request_uri, 'wp-content/plugins/gutenberg-core' ) || $is_preview || $is_preview_overlay ) {
597            return false;
598        }
599
600        return true;
601    }
602
603    /**
604     * Enqueue Agents Manager script based on context.
605     *
606     * @param string $variant The variant of the asset file to get.
607     * @return string|null The deployed build version from the asset file, or null when unavailable.
608     */
609    private function enqueue_script( $variant ) {
610        $cache_key  = 'agents-manager-asset-' . $variant . '.asset.json';
611        $asset_file = get_transient( $cache_key );
612
613        if ( ! $asset_file ) {
614            $asset_file = self::get_assets_json( 'widgets.wp.com/agents-manager/agents-manager-' . $variant . '.asset.json' );
615            if ( ! $asset_file ) {
616                return null;
617            }
618            set_transient( $cache_key, $asset_file, HOUR_IN_SECONDS );
619        }
620
621        // When the request is dev mode, use a random cache buster as the version for easier debugging.
622        $version = self::is_dev_mode() ? wp_rand() : $asset_file['version'];
623
624        $script_dependencies = $asset_file['dependencies'] ?? array();
625
626        // Load translations for connected variants from widgets.wp.com.
627        // Disconnected variants have no translatable UI, so skip them (as Help
628        // Center does). English needs no translation file.
629        if ( ! str_contains( $variant, 'disconnected' ) ) {
630            $locale = self::determine_iso_639_locale();
631
632            if ( 'en' !== $locale ) {
633                wp_enqueue_script(
634                    'agents-manager-translations',
635                    'https://widgets.wp.com/agents-manager/languages/' . $locale . '-v1.js',
636                    array( 'wp-i18n' ),
637                    $version,
638                    true
639                );
640
641                $script_dependencies[] = 'agents-manager-translations';
642            }
643        }
644
645        wp_enqueue_script(
646            'agents-manager',
647            'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . '.min.js',
648            $script_dependencies,
649            $version,
650            /**
651             * Filter the strategy to use when enqueuing the script.
652             *
653             * @param array|bool $args The arguments to pass to wp_enqueue_script. Default is true.
654             * @param string $handle The handle of the script.
655             */
656            apply_filters( 'agents_manager_enqueue_script_strategy', true, 'agents-manager' )
657        );
658
659        if ( 'gutenberg-disconnected' !== $variant && 'ciab-disconnected' !== $variant ) {
660            wp_enqueue_style(
661                'agents-manager-style',
662                'https://widgets.wp.com/agents-manager/agents-manager-' . $variant . ( is_rtl() ? '.rtl.css' : '.css' ),
663                array(),
664                $version
665            );
666        }
667
668        return (string) $asset_file['version'];
669    }
670
671    /**
672     * Returns the ISO 639 conforming locale string for the current user.
673     *
674     * Normalizes the WordPress user locale to match the widgets.wp.com translation
675     * file naming at languages/{code}-v1.js. Preserves the region for the few locales
676     * where it is meaningful (pt-br, zh-tw, zh-cn); strips the region for all others;
677     * falls back to 'en' when the locale is empty.
678     *
679     * @return string The ISO 639 locale string, e.g. "en".
680     */
681    private static function determine_iso_639_locale() {
682        $language = get_user_locale();
683        $language = strtolower( $language );
684
685        if ( in_array( $language, array( 'pt_br', 'pt-br', 'zh_tw', 'zh-tw', 'zh_cn', 'zh-cn' ), true ) ) {
686            $language = str_replace( '_', '-', $language );
687        } else {
688            $language = preg_replace( '/([-_].*)$/i', '', $language );
689        }
690
691        if ( empty( $language ) ) {
692            return 'en';
693        }
694
695        return $language;
696    }
697
698    /**
699     * Get the asset via file-system on wpcom and via network on Atomic sites.
700     *
701     * @param string $filepath The URL to download the asset file from.
702     * @return array|null The asset file data or null on failure.
703     */
704    private static function get_assets_json( $filepath ) {
705        $accessible_directly = file_exists( ABSPATH . $filepath );
706
707        if ( $accessible_directly ) {
708            $file_contents = file_get_contents( ABSPATH . $filepath );
709
710            if ( false === $file_contents ) {
711                return null;
712            }
713
714            return json_decode( $file_contents, true );
715        }
716
717        $request = wp_remote_get( 'https://' . $filepath );
718
719        if ( is_wp_error( $request ) ) {
720            return null;
721        }
722
723        $response_code = wp_remote_retrieve_response_code( $request );
724        if ( 200 !== $response_code ) {
725            return null;
726        }
727
728        $content_type = wp_remote_retrieve_header( $request, 'content-type' );
729        if ( is_string( $content_type ) && false === strpos( $content_type, 'json' ) ) {
730            return null;
731        }
732
733        $body = wp_remote_retrieve_body( $request );
734        if ( '' === $body ) {
735            return null;
736        }
737
738        $decoded = json_decode( $body, true );
739        if ( json_last_error() !== JSON_ERROR_NONE ) {
740            return null;
741        }
742
743        return $decoded;
744    }
745
746    /**
747     * Update the calypso preferences.
748     *
749     * @param \stdClass $preferences The preferences.
750     *
751     * @return \stdClass The preferences.
752     */
753    public function calypso_preferences_update( $preferences ) {
754        // Check if agents_manager_router_history exists and is a valid array structure
755        if ( ! isset( $preferences->agents_manager_router_history ) ||
756            ! is_array( $preferences->agents_manager_router_history ) ) {
757            return $preferences;
758        }
759
760        $router_history = $preferences->agents_manager_router_history;
761
762        // Check if entries exist and is an array
763        if ( ! isset( $router_history['entries'] ) ||
764            ! is_array( $router_history['entries'] ) ) {
765            return $preferences;
766        }
767
768        $entries = $router_history['entries'];
769
770        // Limit entries to 50 to prevent spamming entries in the router history.
771        if ( count( $entries ) > 50 ) {
772            // Keep only the last 49 entries and add the root entry at the beginning.
773            $entries = array_slice( $entries, -49 );
774            // Keep the start at root so the back button always works.
775            array_unshift(
776                $entries,
777                array(
778                    'pathname' => '/',
779                    'search'   => '',
780                    'hash'     => '',
781                    'key'      => 'default',
782                    'state'    => null,
783                )
784            );
785
786            // Update the preferences object directly
787            $preferences->agents_manager_router_history['entries'] = $entries;
788            $preferences->agents_manager_router_history['index']   = 49;
789        }
790
791        return $preferences;
792    }
793
794    /**
795     * Creates instance.
796     *
797     * @return Agents_Manager
798     */
799    public static function init() {
800        if ( did_action( 'jetpack_agents_manager_initialized' ) ) {
801            return self::get_instance();
802        }
803
804        self::$instance = new self();
805
806        /**
807         * Fires once the Agents Manager class has been instantiated.
808         *
809         * @since 0.5.0
810         */
811        do_action( 'jetpack_agents_manager_initialized' );
812
813        return self::$instance;
814    }
815
816    /**
817     * Returns the instance of the Agents Manager class.
818     *
819     * @return Agents_Manager
820     */
821    public static function get_instance() {
822        return self::$instance;
823    }
824
825    /**
826     * Returns whether the current request is coming from the A8C proxy.
827     *
828     * @return bool
829     */
830    private static function is_proxied() {
831        // On Simple sites, use the wpcom function if available.
832        if ( function_exists( 'wpcom_is_proxied_request' ) ) {
833            return wpcom_is_proxied_request();
834        }
835
836        // On WoA/Garden sites, check server variable or constant.
837        return isset( $_SERVER['A8C_PROXIED_REQUEST'] )
838            ? (bool) sanitize_text_field( wp_unslash( $_SERVER['A8C_PROXIED_REQUEST'] ) )
839            : Constants::is_true( 'A8C_PROXIED_REQUEST' );
840    }
841
842    /**
843     * Returns whether the current visitor should be marked as an Automattician in tracking.
844     *
845     * @return bool
846     */
847    private static function is_tracking_automattician() {
848        $is_automattician = function_exists( 'is_automattician' ) && (bool) is_automattician();
849
850        return $is_automattician || self::is_proxied() || Constants::is_true( 'AT_PROXIED_REQUEST' );
851    }
852
853    /**
854     * Enables "Development" features that should be accessible only for admins.
855     */
856    private static function is_dev_mode() {
857        // Known local environments.
858        $domain = wp_parse_url( get_site_url(), PHP_URL_HOST );
859        if (
860            $domain === 'localhost' ||
861            '.jurassic.tube' === stristr( $domain, '.jurassic.tube' ) ||
862            '.jurassic.ninja' === stristr( $domain, '.jurassic.ninja' )
863        ) {
864            return true;
865        }
866
867        // A8C development.
868        if ( self::is_proxied() ) {
869            return true;
870        }
871
872        if ( Constants::is_true( 'AT_PROXIED_REQUEST' ) && Constants::is_defined( 'ATOMIC_CLIENT_ID' ) ) {
873            switch ( Constants::get_constant( 'ATOMIC_CLIENT_ID' ) ) {
874                case 1:
875                case 2:
876                case 3: // Pressable
877                case 32:
878                case 118: // Commerce garden client (ciab)
879                    return true;
880            }
881        }
882
883        return false;
884    }
885
886    /**
887     * Register the Agents Manager endpoints.
888     */
889    public function register_rest_api() {
890        ( new WP_REST_Agents_Manager_Persisted_Open_State() )->register_rest_route();
891        ( new REST_Jetpack_AI_JWT() )->register_rest_route();
892    }
893
894    /**
895     * Determine if user should see unified experience.
896     *
897     * @param bool $use_unified_experience Whether to use unified experience.
898     * @return bool
899     */
900    public function should_use_unified_experience( $use_unified_experience = false ) {
901        // Early return for non-proxied/dev mode requests.
902        // This feature is currently only available to Automattic employees testing via proxy.
903        if ( ! self::is_dev_mode() ) {
904            return false;
905        }
906
907        $user_id = get_current_user_id();
908
909        if ( ! $user_id ) {
910            return false;
911        }
912
913        $is_simple_site = ( new \Automattic\Jetpack\Status\Host() )->is_wpcom_simple();
914        if ( $is_simple_site ) {
915            // On Simple sites, evaluate locally.
916            // Check Automattician and opt-in setting.
917            $is_automattician = function_exists( '\is_automattician' ) && \is_automattician( $user_id );
918            if ( $is_automattician && $this->has_unified_chat_opt_in_enabled( $user_id ) ) {
919                return true;
920            }
921        }
922
923        // On WoA and Garden sites, delegate to wpcom via the /agents-manager/state endpoint.
924        // This avoids duplicating rollout logic and handles cases where
925        // wpcom-specific functions (like get_user_attribute) aren't available.
926        if ( $this->fetch_unified_experience_preference() ) {
927            return true;
928        }
929
930        // Default to false, for now.
931        // 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.
932        return $use_unified_experience;
933    }
934
935    /**
936     * Check if user has enabled unified chat opt-in in their Automattician options.
937     *
938     * This checks the unified_ai_chat calypso preference set via the wpcom profile settings.
939     * Only used on Simple sites where get_user_attribute is available.
940     *
941     * @param int $user_id User ID.
942     *
943     * @return bool
944     */
945    private function has_unified_chat_opt_in_enabled( $user_id ) {
946        if ( ! function_exists( '\get_user_attribute' ) ) {
947            return false;
948        }
949
950        $calypso_prefs = \get_user_attribute( $user_id, 'calypso_preferences' );
951        return ! empty( $calypso_prefs['unified_ai_chat'] );
952    }
953
954    /**
955     * Fetch unified experience preference from wpcom via Jetpack Connection.
956     *
957     * Used on Atomic sites to delegate the decision to wpcom, which has
958     * access to user attributes and can evaluate the rollout logic.
959     *
960     * Calls /agents-manager/state endpoint which is accessible via Jetpack user tokens.
961     *
962     * @return bool Whether user should see unified experience.
963     */
964    private function fetch_unified_experience_preference() {
965        $user_id = get_current_user_id();
966        if ( ! $user_id ) {
967            return false;
968        }
969
970        // Check transient cache first (per-user cache).
971        $cache_key     = 'unified-experience-' . $user_id;
972        $cached_result = get_transient( $cache_key );
973        if ( false !== $cached_result ) {
974            return (bool) $cached_result;
975        }
976
977        // Check if user is connected before making API call.
978        if ( ! ( new Connection_Manager() )->is_user_connected( $user_id ) ) {
979            return false;
980        }
981
982        // Call dedicated agents-manager/state endpoint.
983        $wpcom_request = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user(
984            '/agents-manager/state?key=unified_ai_chat',
985            '2',
986            array( 'method' => 'GET' )
987        );
988
989        if ( is_wp_error( $wpcom_request ) ) {
990            // Cache failures too to avoid hammering the API.
991            set_transient( $cache_key, 0, MINUTE_IN_SECONDS );
992            return false;
993        }
994
995        $response_code = wp_remote_retrieve_response_code( $wpcom_request );
996        if ( 200 !== $response_code ) {
997            set_transient( $cache_key, 0, MINUTE_IN_SECONDS );
998            return false;
999        }
1000
1001        $body         = wp_remote_retrieve_body( $wpcom_request );
1002        $decoded_body = json_decode( $body, true );
1003
1004        // The response is { "unified_ai_chat": true/false } when using key param.
1005        $result = is_array( $decoded_body ) && ! empty( $decoded_body['unified_ai_chat'] );
1006
1007        // Cache for 1 minute.
1008        set_transient( $cache_key, $result ? 1 : 0, MINUTE_IN_SECONDS );
1009
1010        return $result;
1011    }
1012
1013    /**
1014     * Returns true if the current request is on the frontend and the user can edit posts.
1015     *
1016     * Mirrors Help_Center::is_loading_on_frontend().
1017     *
1018     * @return bool True if loading on the frontend for an eligible user.
1019     */
1020    private static function is_loading_on_frontend() {
1021        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
1022        if ( isset( $_GET['na_site_preview'] ) || isset( $_GET['preview_overlay'] ) ) {
1023            return false;
1024        }
1025
1026        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a context check, not a form submission.
1027        if ( isset( $_GET['preview'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['preview'] ) ) ) {
1028            return false;
1029        }
1030
1031        $can_edit_posts = current_user_can( 'edit_posts' ) && is_user_member_of_blog();
1032        return ! is_admin() && ! self::is_block_editor() && $can_edit_posts;
1033    }
1034
1035    /**
1036     * Returns true if the current screen is the block editor.
1037     *
1038     * @return bool True if the current screen is the block editor.
1039     */
1040    private static function is_block_editor() {
1041        if ( ! function_exists( 'get_current_screen' ) ) {
1042            return false;
1043        }
1044
1045        $current_screen = get_current_screen();
1046        // The widgets screen has the block editor but no Gutenberg top bar.
1047        return $current_screen && $current_screen->is_block_editor() && $current_screen->id !== 'widgets';
1048    }
1049
1050    /**
1051     * Returns true when the WordPress admin bar is available in the block editor.
1052     *
1053     * The frontend uses the visible admin bar as its signal to move editor entry points out of the
1054     * Gutenberg toolbar. Use WordPress's matching server-side signal so a classic editor admin bar
1055     * receives those entry points too, not only Gutenberg's experimental omnibar.
1056     *
1057     * @return bool
1058     */
1059    private static function is_admin_bar_in_editor() {
1060        return self::is_block_editor() && is_admin_bar_showing();
1061    }
1062
1063    /**
1064     * Check if current environment is CIAB (Commerce in a Box) / Next Admin.
1065     *
1066     * Uses the same detection method as Help Center: checks if next_admin_init has fired.
1067     *
1068     * @return bool True if CIAB/Next Admin environment.
1069     */
1070    private static function is_ciab_environment() {
1071        return (bool) did_action( 'next_admin_init' );
1072    }
1073
1074    /**
1075     * Returns true if the current user is NOT connected through Jetpack.
1076     *
1077     * Mirrors the logic from Help_Center::is_jetpack_disconnected().
1078     *
1079     * @return bool True if the site uses Jetpack but the current user is not connected.
1080     */
1081    private static function is_jetpack_disconnected() {
1082        $user_id = get_current_user_id();
1083        $blog_id = get_current_blog_id();
1084
1085        if ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) {
1086            return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
1087        }
1088
1089        if ( true === apply_filters( 'is_jetpack_site', false, $blog_id ) ) {
1090            return ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected( $user_id );
1091        }
1092
1093        return false;
1094    }
1095
1096    /**
1097     * Get current user data for the agents manager.
1098     *
1099     * Mirrors the user data structure from Help Center's helpCenterData.
1100     *
1101     * @return array|null User data array or null if not logged in.
1102     */
1103    private function get_current_user_data() {
1104        $user_id = get_current_user_id();
1105        if ( ! $user_id ) {
1106            return null;
1107        }
1108
1109        $user_data = get_userdata( $user_id );
1110        if ( ! $user_data ) {
1111            return null;
1112        }
1113
1114        $user_email = $user_data->user_email;
1115
1116        // Use wpcom_get_avatar_url on Simple sites, fall back to get_avatar_url elsewhere.
1117        if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1118            $avatar_url = wpcom_get_avatar_url( $user_email, 64, '', true )[0];
1119        } else {
1120            $avatar_url = get_avatar_url( $user_id );
1121        }
1122
1123        return array(
1124            'ID'           => $user_id,
1125            'username'     => $user_data->user_login,
1126            'display_name' => $user_data->display_name,
1127            'avatar_URL'   => $avatar_url,
1128            'email'        => $user_email,
1129        );
1130    }
1131
1132    /**
1133     * Get current site data for the agents manager.
1134     *
1135     * Returns minimal site data needed by AgentsManager (ID and domain only).
1136     * Uses jetpack_options['id'] on Atomic sites for the wpcom blog ID.
1137     *
1138     * @return array Site data with ID and domain.
1139     */
1140    private function get_current_site() {
1141        /*
1142         * Atomic sites have the WP.com blog ID stored as a Jetpack option.
1143         * This code deliberately doesn't use `Jetpack_Options::get_option`
1144         * so it works even when Jetpack has not been loaded.
1145         */
1146        $jetpack_options = get_option( 'jetpack_options' );
1147        if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) {
1148            $site_id = (int) $jetpack_options['id'];
1149        } else {
1150            $site_id = get_current_blog_id();
1151        }
1152
1153        return array(
1154            'ID'     => $site_id,
1155            'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
1156        );
1157    }
1158}