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