Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.01% covered (warning)
87.01%
154 / 177
73.08% covered (warning)
73.08%
19 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_AI_Sidebar
87.01% covered (warning)
87.01%
154 / 177
73.08% covered (warning)
73.08%
19 / 26
96.75
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 maybe_enqueue_abilities_script
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
6.02
 get_asset_data_from_file
30.00% covered (danger)
30.00%
3 / 10
0.00% covered (danger)
0.00%
0 / 1
13.57
 get_asset_data_from_remote
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
25.85
 register_provider
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 get_ai_sidebar_asset_data
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 is_ai_editorial_review_enabled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 is_generate_feedback_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_optimize_title_suggestion_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_proofread_content_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_seo_suggestions_enabled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 is_excerpt_suggestion_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_seo_feature
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_seo_tools_usable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_jetpack_ai_sidebar_preview_enabled
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 should_expose_sidebar
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 get_jetpack_ai_sidebar_preview_config
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 is_toolbar_button_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 register_toolbar_button_extension
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 add_agents_manager_data
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 get_sidebar_am_fields
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 enable_agents_manager_in_post_editor
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 maybe_patch_jetpack_ai_sidebar_preview_data
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 is_block_editor
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 is_post_editor
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 has_ai_features
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Jetpack AI Sidebar — Agents Manager integration and provider registration.
4 *
5 * Initializes the jetpack-agents-manager package so it loads the Agents
6 * Manager bundle and emits its `agentsManagerData` payload, registers Jetpack
7 * AI as an Agents Manager provider, and loads the Jetpack AI provider bundle
8 * that exposes Jetpack AI abilities in the block editor.
9 *
10 * @package automattic/jetpack
11 */
12
13namespace Automattic\Jetpack\Extensions\AiAssistantPlugin;
14
15use Automattic\Jetpack\Agents_Manager\Agents_Manager;
16use Automattic\Jetpack\Connection\Manager as Connection_Manager;
17use Automattic\Jetpack\Current_Plan;
18use Automattic\Jetpack\Modules;
19use Automattic\Jetpack\Status;
20use Automattic\Jetpack\Status\Host;
21
22const AM_ASSET_BASE_PATH                  = 'widgets.wp.com/agents-manager/';
23const AI_SIDEBAR_ASSET_TRANSIENT          = 'jetpack_ai_sidebar_asset';
24const AI_SIDEBAR_JS_URL                   = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.min.js';
25const AI_SIDEBAR_CSS_URL                  = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.css';
26const AI_SIDEBAR_RTL_CSS_URL              = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.rtl.css';
27const AI_SIDEBAR_PROVIDER_URL             = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.provider.mjs';
28const AI_SIDEBAR_AGENT_ID                 = 'wp-orchestrator';
29const AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION = 'ai-sidebar-toolbar-button';
30
31/**
32 * Initializes the Agents Manager package and registers the Jetpack AI
33 * provider in the block editor.
34 */
35class Jetpack_AI_Sidebar {
36
37    /**
38     * Initialize hooks.
39     *
40     * @return void
41     */
42    public static function init(): void {
43        // Gate the whole sidebar entrypoint on the preview surface, which is
44        // itself overridable via the jetpack_ai_sidebar_enabled filter.
45        if ( ! self::is_jetpack_ai_sidebar_preview_enabled() ) {
46            return;
47        }
48
49        // Initialize the Agents Manager package so it owns loading the Agents
50        // Manager bundle and emitting the `agentsManagerData` payload. The call
51        // is idempotent: on WordPress.com and Atomic, jetpack-mu-wpcom may have
52        // already initialized it, in which case this is a no-op.
53        Agents_Manager::init();
54
55        // Register as Agents Manager provider. The filter fires inside
56        // Agents_Manager::enqueue_scripts(). Priority 20 so Jetpack loads
57        // AFTER Image Studio (priority 10).
58        add_filter( 'agents_manager_agent_providers', array( __CLASS__, 'register_provider' ), 20 );
59
60        add_filter( 'jetpack_ai_sidebar_agents_manager_data', array( __CLASS__, 'add_agents_manager_data' ), 10, 1 );
61
62        // Ask the Agents Manager package to mount in the post editor on Jetpack
63        // AI Sidebar Preview surfaces.
64        add_filter( 'agents_manager_enabled_in_block_editor', array( __CLASS__, 'enable_agents_manager_in_post_editor' ) );
65
66        // Enqueue the IIFE bundle in the preview post editor — it registers
67        // Jetpack AI abilities via @wordpress/abilities, which Big Sky or the
68        // Agents Manager can discover regardless of which provider system is active.
69        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_abilities_script' ), 201 );
70
71        // Patch Jetpack AI Sidebar Preview data into agentsManagerData when the
72        // Agents Manager bundle was enqueued by an external host (Big Sky on
73        // Atomic, etc.) and the jetpack_ai_sidebar_agents_manager_data filter
74        // never fired. Priority 250 runs after both jetpack-mu-wpcom and the
75        // Agents Manager package enqueue (priority 101).
76        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_patch_jetpack_ai_sidebar_preview_data' ), 250 );
77
78        // Let editor JS know when the Jetpack AI Sidebar toolbar button replaces the legacy AI toolbar.
79        add_action( 'jetpack_register_gutenberg_extensions', array( __CLASS__, 'register_toolbar_button_extension' ), 99 );
80    }
81
82    // ──────────────────────────────────────────────────
83    // Jetpack AI provider bundle
84    // ──────────────────────────────────────────────────
85
86    /**
87     * Enqueue the IIFE bundle that registers Jetpack AI abilities.
88     *
89     * This runs independently of AM/provider registration so preview abilities
90     * are available even when Big Sky standalone is the active UI.
91     *
92     * @return void
93     */
94    public static function maybe_enqueue_abilities_script(): void {
95        if ( ! self::should_expose_sidebar() ) {
96            return;
97        }
98
99        // CIAB (next-admin) has its own AM setup — don't enqueue alongside it.
100        if ( did_action( 'next_admin_init' ) ) {
101            return;
102        }
103
104        // Guard against double-enqueue (e.g. hooked multiple times).
105        if ( wp_script_is( 'jetpack-ai-provider' ) ) {
106            return;
107        }
108
109        $asset_data = self::get_ai_sidebar_asset_data();
110        if ( ! $asset_data ) {
111            return;
112        }
113
114        $version      = $asset_data['version'] ?? false;
115        $dependencies = $asset_data['dependencies'] ?? array();
116
117        wp_enqueue_script(
118            'jetpack-ai-provider',
119            AI_SIDEBAR_JS_URL,
120            $dependencies,
121            $version,
122            true
123        );
124
125        wp_enqueue_style(
126            'jetpack-ai-provider',
127            is_rtl() ? AI_SIDEBAR_RTL_CSS_URL : AI_SIDEBAR_CSS_URL,
128            array(),
129            $version
130        );
131    }
132
133    // ──────────────────────────────────────────────────
134    // Asset manifest (Image Studio pattern)
135    // ──────────────────────────────────────────────────
136
137    /**
138     * Try to read the asset manifest from the local filesystem.
139     *
140     * On WordPress.com, widgets.wp.com assets are available at ABSPATH.
141     *
142     * @param string $relative_path The relative path to the JSON file.
143     * @return array|false The decoded asset data, or false if not available locally.
144     */
145    private static function get_asset_data_from_file( string $relative_path ) {
146        $local_path = ABSPATH . $relative_path;
147        if ( ! file_exists( $local_path ) ) {
148            return false;
149        }
150
151        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a local file, not a remote URL.
152        $contents = file_get_contents( $local_path );
153        if ( false === $contents ) {
154            return false;
155        }
156
157        $data = json_decode( $contents, true );
158        if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
159            return false;
160        }
161
162        return $data;
163    }
164
165    /**
166     * Fetch the asset manifest via HTTP.
167     *
168     * @param string $url The URL to fetch.
169     * @return array|false The decoded asset data, or false on failure.
170     */
171    private static function get_asset_data_from_remote( string $url ) {
172        $response = wp_safe_remote_get( $url );
173        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
174            return false;
175        }
176
177        $content_type = wp_remote_retrieve_header( $response, 'content-type' );
178        if ( is_string( $content_type ) && false === stripos( $content_type, 'json' ) ) {
179            return false;
180        }
181
182        $body = wp_remote_retrieve_body( $response );
183        $data = json_decode( $body, true );
184        if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
185            return false;
186        }
187
188        return $data;
189    }
190
191    // ──────────────────────────────────────────────────
192    // Provider registration
193    // ──────────────────────────────────────────────────
194
195    /**
196     * Register Jetpack AI as an Agents Manager provider.
197     *
198     * Appends the CDN-hosted ESM wrapper URL to the providers list so AM
199     * can dynamically import it. Asset enqueueing is handled separately by
200     * maybe_enqueue_abilities_script.
201     *
202     * @param array $providers Existing provider URLs.
203     * @return array Updated providers.
204     */
205    public static function register_provider( array $providers ): array {
206        // CIAB (next-admin) has AM natively — skip to avoid duplicate agents.
207        if ( did_action( 'next_admin_init' ) ) {
208            return $providers;
209        }
210
211        // The provider IIFE is only enqueued in the post editor. Avoid registering
212        // the ESM wrapper on other block-editor surfaces, where AM may import it
213        // before window.__JetpackAIProvider exists.
214        if ( ! self::should_expose_sidebar() ) {
215            return $providers;
216        }
217
218        // Don't register if the IIFE bundle cannot be loaded. The ESM wrapper
219        // re-exports from window.__JetpackAIProvider at import time; if the
220        // IIFE never ran, toolProvider is still a truthy Proxy and AM would
221        // call getAbilities() on it and get undefined, breaking the merge.
222        if ( ! self::get_ai_sidebar_asset_data() ) {
223            return $providers;
224        }
225
226        // Register as AM provider via CDN-hosted ESM wrapper.
227        // AM dynamically imports this module to merge tools, suggestions, and components.
228        // No ?ver= needed — the wrapper re-exports from window.__JetpackAIProvider
229        // at import time, so its behavior always matches the loaded IIFE bundle.
230        $providers[] = AI_SIDEBAR_PROVIDER_URL;
231
232        return $providers;
233    }
234
235    /**
236     * Fetch and cache the CDN asset manifest for the AI sidebar bundle.
237     *
238     * @return array|false The decoded asset data, or false on failure.
239     */
240    private static function get_ai_sidebar_asset_data() {
241        $skip_cache = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG;
242
243        if ( ! $skip_cache ) {
244            $cached = get_transient( AI_SIDEBAR_ASSET_TRANSIENT );
245            if ( false !== $cached ) {
246                return $cached;
247            }
248        }
249
250        $json_path = AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.asset.json';
251
252        // Try local file first (available on WordPress.com).
253        $data = self::get_asset_data_from_file( $json_path );
254
255        // Fallback to remote fetch.
256        if ( false === $data ) {
257            $data = self::get_asset_data_from_remote( 'https://' . $json_path );
258        }
259
260        if ( false === $data ) {
261            // The manifest is unavailable from both the local file and the CDN
262            // — for example when the server cannot reach widgets.wp.com. Skip
263            // rather than enqueue a provider whose bundle the browser may also
264            // be unable to load, which would break the Agents Manager merge.
265            return false;
266        }
267
268        if ( ! $skip_cache ) {
269            set_transient( AI_SIDEBAR_ASSET_TRANSIENT, $data, HOUR_IN_SECONDS );
270        }
271
272        return $data;
273    }
274
275    // ──────────────────────────────────────────────────
276    // Helper methods
277    // ──────────────────────────────────────────────────
278
279    /**
280     * UI feature flag for AI Editorial Review.
281     *
282     * Server-side permission checks still gate execution. This site-side flag
283     * controls whether the sidebar suggestion is exposed, while keeping a
284     * feature-specific filter available as a kill switch.
285     *
286     * @return bool
287     */
288    private static function is_ai_editorial_review_enabled(): bool {
289        return (bool) apply_filters(
290            'jetpack_ai_editorial_review_enabled',
291            true
292        );
293    }
294
295    /**
296     * UI feature flag for the Generate Feedback suggestion.
297     *
298     * Exposed only in internal testing environments while the feature is in development.
299     *
300     * @return bool
301     */
302    private static function is_generate_feedback_enabled(): bool {
303        return jetpack_is_internal_testing_environment();
304    }
305
306    /**
307     * UI feature flag for the Optimize Title suggestion.
308     *
309     * Exposed only in internal testing environments while the feature is in development.
310     *
311     * @return bool
312     */
313    private static function is_optimize_title_suggestion_enabled(): bool {
314        return jetpack_is_internal_testing_environment();
315    }
316
317    /**
318     * UI feature flag for Proofreader (spelling and grammar).
319     *
320     * Server-side permission checks still gate execution. This site-side flag
321     * controls whether the Jetpack AI Sidebar exposes the Proofreader
322     * suggestion. It follows Image Studio's internal rollout pattern.
323     *
324     * @return bool
325     */
326    private static function is_proofread_content_enabled(): bool {
327        return jetpack_is_internal_testing_environment();
328    }
329
330    /**
331     * UI feature flag for the SEO Enhancer suggestions (SEO title and meta description).
332     *
333     * Exposed only in internal testing environments while the feature is in development,
334     * and only where the suggestions can actually be used: the SEO Enhancer is not
335     * killed via its filter, the site's plan includes the Jetpack SEO feature (the
336     * suggestions write to the plan-gated SEO title and meta description fields), and
337     * SEO tools are usable on the site. Kept independent of the Optimize Title flag:
338     * SEO suggestions target the SEO meta fields, not the visible post title.
339     *
340     * The user-facing ai_seo_enhancer_enabled *option* is deliberately not consulted —
341     * it only governs automatic generation on publish, while these suggestions are
342     * user-initiated.
343     *
344     * @return bool
345     */
346    private static function is_seo_suggestions_enabled(): bool {
347        return jetpack_is_internal_testing_environment()
348            && (bool) apply_filters( 'ai_seo_enhancer_enabled', true )
349            && self::has_seo_feature()
350            && self::is_seo_tools_usable();
351    }
352
353    /**
354     * UI feature flag for the Generate Excerpt suggestion.
355     *
356     * Exposed only in internal testing environments while the feature is in development.
357     * No plan gate: the excerpt is a core editorial field, and the ability's own
358     * permission callback (edit_posts) gates execution server-side.
359     *
360     * @return bool
361     */
362    private static function is_excerpt_suggestion_enabled(): bool {
363        return jetpack_is_internal_testing_environment();
364    }
365
366    /**
367     * Whether the site's plan includes the Jetpack SEO feature.
368     *
369     * Same predicate the SEO editor panel uses to decide between the SEO fields and
370     * the "Optimize SEO" upgrade nudge: extensions/plugins/seo/seo.php registers
371     * availability via Jetpack_Gutenberg::set_availability_for_plan( 'advanced-seo' ),
372     * which resolves through Current_Plan::supports(). On WordPress.com Simple and
373     * Atomic this delegates to wpcom_site_has_feature( 'advanced-seo' ) — Business
374     * and higher plans; on self-hosted sites every plan includes the feature.
375     *
376     * @return bool
377     */
378    private static function has_seo_feature(): bool {
379        return Current_Plan::supports( 'advanced-seo' );
380    }
381
382    /**
383     * Whether Jetpack SEO tools are usable on this site: SEO is not disabled via the
384     * jetpack_disable_seo_tools filter — which the seo-tools module enables itself
385     * when a conflicting SEO plugin (Yoast, AIOSEO, Rank Math, …) owns the site's
386     * SEO — and the seo-tools module is active, since the module registers the SEO
387     * meta fields the suggestions write to. On WordPress.com Simple the module always
388     * reports active, so there this reduces to the filter check.
389     *
390     * @return bool
391     */
392    private static function is_seo_tools_usable(): bool {
393        /** This filter is documented in modules/seo-tools/class-jetpack-seo-utils.php */
394        return ! apply_filters( 'jetpack_disable_seo_tools', false )
395            && ( new Modules() )->is_active( 'seo-tools' );
396    }
397
398    /**
399     * UI feature flag for the public Jetpack AI Sidebar Preview surface.
400     *
401     * Defaults to enabled only on WordPress.com platform sites (Simple or WoA)
402     * that have the Big Sky plugin present and enabled. Big Sky defaults on for
403     * Simple sites and off on WoA/Atomic. The jetpack_ai_sidebar_enabled filter
404     * is a host-level override of that default, respected by init() and every
405     * sidebar surface that gates on this method.
406     *
407     * @return bool
408     */
409    private static function is_jetpack_ai_sidebar_preview_enabled(): bool {
410        $host = new Host();
411
412        $enabled = false;
413        if ( $host->is_wpcom_platform() && class_exists( 'Big_Sky' ) ) {
414            $default = $host->is_wpcom_simple() ? '1' : '0';
415            $enabled = (bool) get_option( 'big_sky_enable', $default );
416        }
417
418        /**
419         * Filter to enable or disable the Jetpack AI sidebar feature.
420         *
421         * Defaults to true only on WordPress.com platform sites with Big Sky
422         * present and enabled. Acts as a host-level override that can force the
423         * sidebar on (e.g. for local development) or off, and is respected by
424         * init() and every sidebar surface.
425         *
426         * @param bool $enabled Whether the Jetpack AI sidebar is enabled.
427         */
428        return (bool) apply_filters( 'jetpack_ai_sidebar_enabled', $enabled );
429    }
430
431    /**
432     * Whether the sidebar surface should be exposed for this request: the sidebar
433     * gate is open, we are in the post editor, and AI features are available.
434     *
435     * @return bool
436     */
437    private static function should_expose_sidebar(): bool {
438        return self::is_jetpack_ai_sidebar_preview_enabled() && self::is_post_editor() && self::has_ai_features();
439    }
440
441    /**
442     * Preview configuration consumed by the Agents Manager and Jetpack AI provider bundles.
443     *
444     * @return array Preview mode and feature availability.
445     */
446    private static function get_jetpack_ai_sidebar_preview_config(): array {
447        $features = array(
448            'aiEditorialReview'       => self::is_ai_editorial_review_enabled(),
449            'generateFeedback'        => self::is_generate_feedback_enabled(),
450            'proofreadContent'        => self::is_proofread_content_enabled(),
451            'blockTransformations'    => true,
452            'blockToolbarButton'      => false,
453            'optimizeTitleSuggestion' => self::is_optimize_title_suggestion_enabled(),
454            'seoSuggestions'          => self::is_seo_suggestions_enabled(),
455            'excerptSuggestion'       => self::is_excerpt_suggestion_enabled(),
456            'chatHistory'             => false,
457            'supportGuides'           => false,
458        );
459
460        /**
461         * Filter the feature set exposed in Jetpack AI Sidebar Preview.
462         *
463         * @param array $features Associative array of preview feature flags.
464         */
465        $filtered_features = apply_filters( 'jetpack_ai_sidebar_preview_features', $features );
466        $features          = is_array( $filtered_features ) ? array_merge( $features, $filtered_features ) : $features;
467
468        // Re-assert the testing-environment gates so the generic features filter cannot
469        // expose in-development suggestions outside internal testing environments.
470        $features['generateFeedback']        = self::is_generate_feedback_enabled();
471        $features['proofreadContent']        = self::is_proofread_content_enabled();
472        $features['optimizeTitleSuggestion'] = (bool) $features['optimizeTitleSuggestion'] && self::is_optimize_title_suggestion_enabled();
473        $features['seoSuggestions']          = (bool) $features['seoSuggestions'] && self::is_seo_suggestions_enabled();
474        $features['excerptSuggestion']       = (bool) $features['excerptSuggestion'] && self::is_excerpt_suggestion_enabled();
475
476        return array(
477            'enabled'  => self::is_jetpack_ai_sidebar_preview_enabled(),
478            'features' => $features,
479        );
480    }
481
482    /**
483     * Whether the Jetpack AI Sidebar toolbar button replaces the legacy AI toolbar.
484     *
485     * @return bool
486     */
487    public static function is_toolbar_button_enabled(): bool {
488        $preview_config = self::get_jetpack_ai_sidebar_preview_config();
489
490        return self::should_expose_sidebar()
491            && true === ( $preview_config['features']['blockToolbarButton'] ?? false );
492    }
493
494    /**
495     * Register the Jetpack AI Sidebar toolbar button feature.
496     *
497     * @return void
498     */
499    public static function register_toolbar_button_extension(): void {
500        if ( ! self::is_toolbar_button_enabled() ) {
501            \Jetpack_Gutenberg::set_extension_unavailable(
502                AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION,
503                'jetpack_ai_sidebar_feature_disabled'
504            );
505            return;
506        }
507
508        \Jetpack_Gutenberg::set_extension_available( AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION );
509    }
510
511    /**
512     * Add Jetpack AI Sidebar-specific data to externally emitted Agents Manager payloads.
513     *
514     * @param mixed $data Data encoded into `agentsManagerData`.
515     * @return mixed Filtered data.
516     */
517    public static function add_agents_manager_data( $data ) {
518        if ( ! is_array( $data ) ) {
519            return $data;
520        }
521
522        if ( ! self::should_expose_sidebar() ) {
523            return $data;
524        }
525
526        // Set our fields in place, leaving the rest of $data (including agentProviders)
527        // untouched so the client-side gate can drop Jetpack AI Sidebar while keeping
528        // fallbacks such as the Big Sky provider. Hosts that need intentional overrides
529        // should use the AI Editorial Review and preview filters.
530        foreach ( self::get_sidebar_am_fields() as $key => $value ) {
531            $data[ $key ] = $value;
532        }
533        return $data;
534    }
535
536    /**
537     * Fields Jetpack contributes to `agentsManagerData`. Single source shared by the
538     * data filter and the external-AM inline fallback so the two cannot drift.
539     *
540     * @return array
541     */
542    private static function get_sidebar_am_fields(): array {
543        $config = self::get_jetpack_ai_sidebar_preview_config();
544
545        return array(
546            'agentId'          => AI_SIDEBAR_AGENT_ID,
547            'jetpackAiSidebar' => $config,
548        );
549    }
550
551    /**
552     * Enable Agents Manager in the post editor when Jetpack AI Sidebar Preview is available.
553     *
554     * @param mixed $enabled Existing Agents Manager block-editor gate value.
555     * @return bool
556     */
557    public static function enable_agents_manager_in_post_editor( $enabled ): bool {
558        if ( $enabled ) {
559            return true;
560        }
561
562        return self::should_expose_sidebar();
563    }
564
565    /**
566     * Inject Jetpack AI Sidebar Preview data into an externally enqueued AM bundle.
567     *
568     * The design-intended hook is jetpack_ai_sidebar_agents_manager_data, applied
569     * by jetpack-agents-manager Agents_Manager::enqueue_scripts(). On Atomic the bundled
570     * mu-wpcom (via wpcomsh) lags this PR, so the filter never fires and the
571     * client gets agentsManagerData without our fields. This `before` script runs
572     * after the upstream `before` that declares the const (added earlier) but
573     * before the AM bundle reads it, so the field is set when AM initialises.
574     * Gives Atomic parity with Jurassic Ninja without depending on a wpcomsh
575     * redeploy.
576     *
577     * Skipped on WordPress.com Simple — wpcom's data extension owns the predicate
578     * there, including any WordPress.com-specific kill-switch override.
579     *
580     * @return void
581     */
582    public static function maybe_patch_jetpack_ai_sidebar_preview_data(): void {
583        if ( ( new Host() )->is_wpcom_simple() ) {
584            return;
585        }
586        if ( ! self::should_expose_sidebar() ) {
587            return;
588        }
589        // 'registered' rather than 'enqueued': wp_add_inline_script attaches to any
590        // registered handle and serializes correctly regardless of when the
591        // enqueue lands in the dependency graph.
592        if ( ! wp_script_is( 'agents-manager', 'registered' ) ) {
593            return;
594        }
595
596        // Build the assignments from the same field source as the data filter so the
597        // two emit paths cannot drift. agentProviders is left untouched so client-side
598        // gating can fall back to other providers (such as Big Sky) when Jetpack AI
599        // Sidebar is unavailable.
600        $assignments = '';
601        foreach ( self::get_sidebar_am_fields() as $key => $value ) {
602            $assignments .= ' agentsManagerData.' . $key . ' = '
603                . wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
604        }
605
606        wp_add_inline_script(
607            'agents-manager',
608            'if ( typeof agentsManagerData === "object" && agentsManagerData !== null ) {' . $assignments . ' }',
609            'before'
610        );
611    }
612
613    /**
614     * Check if the current screen is a block editor.
615     *
616     * @return bool
617     */
618    private static function is_block_editor(): bool {
619        if ( ! function_exists( 'get_current_screen' ) ) {
620            return false;
621        }
622
623        $screen = get_current_screen();
624        return $screen && $screen->is_block_editor();
625    }
626
627    /**
628     * Check if the current screen is the post block editor.
629     *
630     * @return bool
631     */
632    private static function is_post_editor(): bool {
633        if ( ! self::is_block_editor() ) {
634            return false;
635        }
636
637        $screen = get_current_screen();
638        return $screen instanceof \WP_Screen
639            && 'post' === $screen->base
640            && 'post' === $screen->post_type;
641    }
642
643    /**
644     * Check whether AI features are available.
645     *
646     * - wpcom simple: always available.
647     * - Atomic/self-hosted: requires a connected owner with AI not disabled.
648     *
649     * @return bool
650     */
651    private static function has_ai_features(): bool {
652        $host = new Host();
653
654        if ( $host->is_wpcom_simple() ) {
655            return true;
656        }
657
658        return ( new Connection_Manager( 'jetpack' ) )->has_connected_owner()
659            && ! ( new Status() )->is_offline_mode()
660            && apply_filters( 'jetpack_ai_enabled', true );
661    }
662}