diff --git a/projects/plugins/jetpack/changelog/hide-legacy-ai-toolbar-with-sidebar b/projects/plugins/jetpack/changelog/hide-legacy-ai-toolbar-with-sidebar new file mode 100644 index 000000000000..01074d49ea25 --- /dev/null +++ b/projects/plugins/jetpack/changelog/hide-legacy-ai-toolbar-with-sidebar @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +AI Assistant: Hide legacy block toolbar controls when Jetpack AI Sidebar content editing is enabled. diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/image/with-ai-image-extension.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/image/with-ai-image-extension.tsx index 06605b84aa7c..53b8aa3ef10f 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/image/with-ai-image-extension.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/image/with-ai-image-extension.tsx @@ -21,7 +21,10 @@ import debugFactory from 'debug'; import { store as seoStore } from '../../../../plugins/ai-assistant-plugin/components/seo-enhancer/store'; import useBlockModuleStatus from '../../hooks/use-block-module-status'; import { getFeatureAvailability } from '../../lib/utils/get-feature-availability'; -import { canAIAssistantBeEnabled } from '../lib/can-ai-assistant-be-enabled'; +import { + canAIAssistantBeEnabled, + isAiSidebarToolbarButtonEnabled, +} from '../lib/can-ai-assistant-be-enabled'; import { preprocessImageContent } from '../lib/preprocess-image-content'; import { TYPE_ALT_TEXT, TYPE_CAPTION } from '../types'; import AiAssistantImageExtensionToolbarDropdown from './components/image-toolbar-dropdown'; @@ -231,16 +234,18 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { return ( <> - - request( TYPE_ALT_TEXT ) } - onRequestCaption={ () => request( TYPE_CAPTION ) } - loadingAltText={ loadingAltText } - loadingCaption={ loadingCaption } - disabled={ ! hasImage } - wrapperRef={ wrapperRef } - /> - + { ! isAiSidebarToolbarButtonEnabled && ( + + request( TYPE_ALT_TEXT ) } + onRequestCaption={ () => request( TYPE_CAPTION ) } + loadingAltText={ loadingAltText } + loadingCaption={ loadingCaption } + disabled={ ! hasImage } + wrapperRef={ wrapperRef } + /> + + ) } ); } diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/lib/can-ai-assistant-be-enabled.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/lib/can-ai-assistant-be-enabled.ts index 2c2d2ec72628..effd03a11041 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/lib/can-ai-assistant-be-enabled.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/lib/can-ai-assistant-be-enabled.ts @@ -10,9 +10,11 @@ import { select } from '@wordpress/data'; import { getFeatureAvailability } from '../../lib/utils/get-feature-availability'; export const AI_ASSISTANT_SUPPORT_NAME = 'ai-assistant-support'; +export const AI_SIDEBAR_TOOLBAR_BUTTON = 'ai-sidebar-toolbar-button'; // Check if the AI Assistant support is enabled. export const isAiAssistantSupportEnabled = getFeatureAvailability( AI_ASSISTANT_SUPPORT_NAME ); +export const isAiSidebarToolbarButtonEnabled = getFeatureAvailability( AI_SIDEBAR_TOOLBAR_BUTTON ); /** * Check if it is possible to enable the AI Assistant block and its features. diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/text-blocks/with-ai-text-extension.tsx b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/text-blocks/with-ai-text-extension.tsx index dec87d5785c9..d30d9455d21a 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/text-blocks/with-ai-text-extension.tsx +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/extensions/text-blocks/with-ai-text-extension.tsx @@ -22,6 +22,7 @@ import debugFactory from 'debug'; import useAutoScroll from '../../hooks/use-auto-scroll'; import useBlockModuleStatus from '../../hooks/use-block-module-status'; import { mapInternalPromptTypeToBackendPromptType } from '../../lib/prompt/backend-prompt'; +import { isAiSidebarToolbarButtonEnabled } from '../lib/can-ai-assistant-be-enabled'; import AiAssistantInput from './components/ai-assistant-input'; import AiAssistantExtensionToolbarDropdown from './components/ai-assistant-toolbar-dropdown'; import { getBlockHandler, InlineExtensionsContext } from './get-block-handler'; @@ -561,14 +562,16 @@ const blockEditWithAiComponents = createHigherOrderComponent( BlockEdit => { /> ) } - - - + { ! isAiSidebarToolbarButtonEnabled && ( + + + + ) } ); diff --git a/projects/plugins/jetpack/extensions/index.json b/projects/plugins/jetpack/extensions/index.json index 4d8417cd26c2..639bc2e1c46f 100644 --- a/projects/plugins/jetpack/extensions/index.json +++ b/projects/plugins/jetpack/extensions/index.json @@ -74,6 +74,7 @@ "ai-title-optimization-keywords-support", "ai-response-feedback", "ai-assistant-image-extension", + "ai-sidebar-toolbar-button", "ai-seo-enhancer", "ai-correct-spelling", "paypal-payment-buttons", diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/ai-sidebar/class-jetpack-ai-sidebar.php b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/ai-sidebar/class-jetpack-ai-sidebar.php index 019c4bacb747..4872c35d5a82 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/ai-sidebar/class-jetpack-ai-sidebar.php +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/ai-sidebar/class-jetpack-ai-sidebar.php @@ -17,13 +17,14 @@ use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; -const AM_ASSET_BASE_PATH = 'widgets.wp.com/agents-manager/'; -const AI_SIDEBAR_ASSET_TRANSIENT = 'jetpack_ai_sidebar_asset'; -const AI_SIDEBAR_JS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.min.js'; -const AI_SIDEBAR_CSS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.css'; -const AI_SIDEBAR_RTL_CSS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.rtl.css'; -const AI_SIDEBAR_PROVIDER_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.provider.mjs'; -const AI_SIDEBAR_AGENT_ID = 'wp-orchestrator'; +const AM_ASSET_BASE_PATH = 'widgets.wp.com/agents-manager/'; +const AI_SIDEBAR_ASSET_TRANSIENT = 'jetpack_ai_sidebar_asset'; +const AI_SIDEBAR_JS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.min.js'; +const AI_SIDEBAR_CSS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.css'; +const AI_SIDEBAR_RTL_CSS_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.rtl.css'; +const AI_SIDEBAR_PROVIDER_URL = 'https://' . AM_ASSET_BASE_PATH . 'jetpack-ai-sidebar.provider.mjs'; +const AI_SIDEBAR_AGENT_ID = 'wp-orchestrator'; +const AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION = 'ai-sidebar-toolbar-button'; /** * Initializes the Agents Manager package and registers the Jetpack AI @@ -71,6 +72,9 @@ public static function init(): void { // never fired. Priority 250 runs after both jetpack-mu-wpcom and the // Agents Manager package enqueue (priority 101). add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_patch_jetpack_ai_sidebar_preview_data' ), 250 ); + + // Let editor JS know when the Jetpack AI Sidebar toolbar button replaces the legacy AI toolbar. + add_action( 'jetpack_register_gutenberg_extensions', array( __CLASS__, 'register_toolbar_button_extension' ), 99 ); } // ────────────────────────────────────────────────── @@ -361,6 +365,7 @@ private static function get_jetpack_ai_sidebar_preview_config(): array { 'aiEditorialReview' => self::is_ai_editorial_review_enabled(), 'generateFeedback' => self::is_generate_feedback_enabled(), 'blockTransformations' => true, + 'blockToolbarButton' => false, 'optimizeTitleSuggestion' => self::is_optimize_title_suggestion_enabled(), 'chatHistory' => false, 'supportGuides' => false, @@ -385,6 +390,35 @@ private static function get_jetpack_ai_sidebar_preview_config(): array { ); } + /** + * Whether the Jetpack AI Sidebar toolbar button replaces the legacy AI toolbar. + * + * @return bool + */ + public static function is_toolbar_button_enabled(): bool { + $preview_config = self::get_jetpack_ai_sidebar_preview_config(); + + return self::should_expose_sidebar() + && true === ( $preview_config['features']['blockToolbarButton'] ?? false ); + } + + /** + * Register the Jetpack AI Sidebar toolbar button feature. + * + * @return void + */ + public static function register_toolbar_button_extension(): void { + if ( ! self::is_toolbar_button_enabled() ) { + \Jetpack_Gutenberg::set_extension_unavailable( + AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION, + 'jetpack_ai_sidebar_feature_disabled' + ); + return; + } + + \Jetpack_Gutenberg::set_extension_available( AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ); + } + /** * Add Jetpack AI Sidebar-specific data to externally emitted Agents Manager payloads. * diff --git a/projects/plugins/jetpack/tests/php/extensions/plugins/ai-sidebar/Jetpack_AI_Sidebar_Test.php b/projects/plugins/jetpack/tests/php/extensions/plugins/ai-sidebar/Jetpack_AI_Sidebar_Test.php index 73454c6c5539..538a0287292b 100644 --- a/projects/plugins/jetpack/tests/php/extensions/plugins/ai-sidebar/Jetpack_AI_Sidebar_Test.php +++ b/projects/plugins/jetpack/tests/php/extensions/plugins/ai-sidebar/Jetpack_AI_Sidebar_Test.php @@ -52,6 +52,7 @@ class Jetpack_AI_Sidebar_Test extends WP_UnitTestCase { public function set_up() { parent::set_up(); $this->reset_sidebar_hooks(); + \Jetpack_Gutenberg::reset(); add_filter( 'jetpack_offline_mode', '__return_false' ); update_option( 'jetpack_offline_mode', '0' ); Status_Cache::clear(); @@ -93,6 +94,7 @@ public function tear_down() { $GLOBALS['current_screen'] = $this->saved_screen; $GLOBALS['wp_scripts'] = $this->saved_wp_scripts; $GLOBALS['wp_styles'] = $this->saved_wp_styles; + \Jetpack_Gutenberg::reset(); parent::tear_down(); } @@ -107,8 +109,31 @@ private function reset_sidebar_hooks() { remove_all_filters( 'jetpack_ai_sidebar_preview_enabled' ); remove_all_filters( 'jetpack_ai_sidebar_preview_features' ); remove_all_filters( 'jetpack_ai_sidebar_agents_manager_data' ); + remove_filter( 'jetpack_is_connection_ready', '__return_true', 1000 ); + remove_filter( 'jetpack_gutenberg', '__return_true' ); + remove_filter( 'jetpack_set_available_extensions', array( __CLASS__, 'get_sidebar_extension_allowlist' ) ); remove_action( 'admin_enqueue_scripts', array( Jetpack_AI_Sidebar::class, 'maybe_enqueue_abilities_script' ), 201 ); remove_action( 'admin_enqueue_scripts', array( Jetpack_AI_Sidebar::class, 'maybe_patch_jetpack_ai_sidebar_preview_data' ), 250 ); + remove_action( 'jetpack_register_gutenberg_extensions', array( Jetpack_AI_Sidebar::class, 'register_toolbar_button_extension' ), 99 ); + } + + /** + * Limit Jetpack Gutenberg availability checks to the sidebar extension under test. + * + * @return array + */ + public static function get_sidebar_extension_allowlist() { + return array( AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ); + } + + /** + * Enable Jetpack Gutenberg availability checks for the sidebar extension under test. + */ + private function enable_sidebar_extension_availability_checks() { + add_filter( 'jetpack_is_connection_ready', '__return_true', 1000 ); + add_filter( 'jetpack_gutenberg', '__return_true' ); + add_filter( 'jetpack_set_available_extensions', array( __CLASS__, 'get_sidebar_extension_allowlist' ) ); + Status_Cache::clear(); } /** @@ -192,6 +217,14 @@ private function simulate_self_hosted() { Status_Cache::clear(); } + /** + * Mark legacy Jetpack AI block toolbar extensions as available. + */ + private function make_legacy_block_toolbar_extensions_available() { + \Jetpack_Gutenberg::set_extension_available( 'ai-assistant-support' ); + \Jetpack_Gutenberg::set_extension_available( 'ai-assistant-image-extension' ); + } + /** * Simulate the Big_Sky class existing, as it does when the Big Sky plugin is present. */ @@ -285,6 +318,10 @@ public function test_init_registers_hooks_by_default() { has_action( 'admin_enqueue_scripts', array( Jetpack_AI_Sidebar::class, 'maybe_patch_jetpack_ai_sidebar_preview_data' ) ), 'maybe_patch_jetpack_ai_sidebar_preview_data should be hooked by default.' ); + $this->assertNotFalse( + has_action( 'jetpack_register_gutenberg_extensions', array( Jetpack_AI_Sidebar::class, 'register_toolbar_button_extension' ) ), + 'register_toolbar_button_extension should be hooked by default.' + ); } /** @@ -302,6 +339,10 @@ public function test_init_does_nothing_when_filter_is_false() { has_filter( 'agents_manager_enabled_in_block_editor', array( Jetpack_AI_Sidebar::class, 'enable_agents_manager_in_post_editor' ) ), 'enable_agents_manager_in_post_editor should not be hooked when filter is false.' ); + $this->assertFalse( + has_action( 'jetpack_register_gutenberg_extensions', array( Jetpack_AI_Sidebar::class, 'register_toolbar_button_extension' ) ), + 'register_toolbar_button_extension should not be hooked when filter is false.' + ); } /** @@ -320,6 +361,10 @@ public function test_init_does_nothing_on_self_hosted() { has_filter( 'agents_manager_enabled_in_block_editor', array( Jetpack_AI_Sidebar::class, 'enable_agents_manager_in_post_editor' ) ), 'enable_agents_manager_in_post_editor should not be hooked on a self-hosted site.' ); + $this->assertFalse( + has_action( 'jetpack_register_gutenberg_extensions', array( Jetpack_AI_Sidebar::class, 'register_toolbar_button_extension' ) ), + 'register_toolbar_button_extension should not be hooked when the preview gate is false.' + ); } /** @@ -366,6 +411,10 @@ public function test_init_registers_hooks_when_enabled() { has_action( 'admin_enqueue_scripts', array( Jetpack_AI_Sidebar::class, 'maybe_patch_jetpack_ai_sidebar_preview_data' ) ), 'maybe_patch_jetpack_ai_sidebar_preview_data should be hooked when filter is true.' ); + $this->assertNotFalse( + has_action( 'jetpack_register_gutenberg_extensions', array( Jetpack_AI_Sidebar::class, 'register_toolbar_button_extension' ) ), + 'register_toolbar_button_extension should be hooked when filter is true.' + ); } // ────────────────────────────────────────────────── @@ -456,6 +505,121 @@ public function test_preview_filter_overrides_gate() { $this->assertFalse( $this->gate_open() ); } + // ────────────────────────────────────────────────── + // Sidebar toolbar button tests + // ────────────────────────────────────────────────── + + /** + * Test that the toolbar button stays disabled until its preview feature is released. + */ + public function test_toolbar_button_disabled_by_default() { + $this->set_block_editor_screen(); + + $this->assertFalse( Jetpack_AI_Sidebar::is_toolbar_button_enabled() ); + } + + /** + * Test that the preview feature flag activates the toolbar button. + */ + public function test_toolbar_button_enabled_by_preview_feature_flag() { + $this->set_block_editor_screen(); + add_filter( + 'jetpack_ai_sidebar_preview_features', + static function ( $features ) { + $features['blockToolbarButton'] = true; + return $features; + } + ); + + $this->assertTrue( Jetpack_AI_Sidebar::is_toolbar_button_enabled() ); + } + + /** + * Test that the sidebar kill switch disables the toolbar button. + */ + public function test_toolbar_button_respects_sidebar_kill_switch() { + $this->set_block_editor_screen(); + add_filter( + 'jetpack_ai_sidebar_preview_features', + static function ( $features ) { + $features['blockToolbarButton'] = true; + return $features; + } + ); + add_filter( 'jetpack_ai_sidebar_enabled', '__return_false' ); + + $this->assertFalse( Jetpack_AI_Sidebar::is_toolbar_button_enabled() ); + } + + /** + * Test that the active sidebar registers the toolbar button feature. + */ + public function test_register_toolbar_button_extension_marks_feature_available() { + $this->set_block_editor_screen(); + $this->make_legacy_block_toolbar_extensions_available(); + $this->enable_sidebar_extension_availability_checks(); + add_filter( 'jetpack_ai_sidebar_enabled', '__return_true' ); + add_filter( 'jetpack_ai_sidebar_preview_enabled', '__return_true' ); + add_filter( + 'jetpack_ai_sidebar_preview_features', + static function ( $features ) { + $features['blockToolbarButton'] = true; + return $features; + } + ); + + Jetpack_AI_Sidebar::register_toolbar_button_extension(); + + $this->assertTrue( \Jetpack_Gutenberg::is_available( AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ) ); + $this->assertTrue( \Jetpack_Gutenberg::is_available( 'ai-assistant-support' ) ); + $this->assertTrue( \Jetpack_Gutenberg::is_available( 'ai-assistant-image-extension' ) ); + $this->assertTrue( \Jetpack_Gutenberg::get_availability()[ AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ]['available'] ); + } + + /** + * Test that the toolbar button feature stays unavailable until the preview feature is released. + */ + public function test_register_toolbar_button_extension_skips_when_toolbar_button_disabled() { + $this->set_block_editor_screen(); + $this->make_legacy_block_toolbar_extensions_available(); + $this->enable_sidebar_extension_availability_checks(); + add_filter( + 'jetpack_ai_sidebar_preview_features', + static function ( $features ) { + $features['blockToolbarButton'] = false; + return $features; + } + ); + + Jetpack_AI_Sidebar::register_toolbar_button_extension(); + + $this->assertFalse( \Jetpack_Gutenberg::is_available( AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ) ); + $availability = \Jetpack_Gutenberg::get_availability()[ AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ]; + $this->assertFalse( $availability['available'] ); + $this->assertSame( 'jetpack_ai_sidebar_feature_disabled', $availability['unavailable_reason'] ); + } + + /** + * Test that the toolbar button feature stays unavailable outside the post editor. + */ + public function test_register_toolbar_button_extension_skips_page_editor() { + $this->set_page_block_editor_screen(); + $this->make_legacy_block_toolbar_extensions_available(); + add_filter( + 'jetpack_ai_sidebar_preview_features', + static function ( $features ) { + $features['blockToolbarButton'] = true; + return $features; + } + ); + + Jetpack_AI_Sidebar::register_toolbar_button_extension(); + + $this->assertFalse( \Jetpack_Gutenberg::is_available( AiAssistantPlugin\AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION ) ); + $this->assertTrue( \Jetpack_Gutenberg::is_available( 'ai-assistant-support' ) ); + $this->assertTrue( \Jetpack_Gutenberg::is_available( 'ai-assistant-image-extension' ) ); + } + // ────────────────────────────────────────────────── // agents_manager_enabled_in_block_editor() tests // ────────────────────────────────────────────────── @@ -529,6 +693,7 @@ public function test_add_agents_manager_data_exposes_ai_editorial_review_enabled $this->assertSame( true, $data['jetpackAiSidebar']['enabled'] ); $this->assertSame( true, $data['jetpackAiSidebar']['features']['aiEditorialReview'] ); $this->assertSame( true, $data['jetpackAiSidebar']['features']['blockTransformations'] ); + $this->assertSame( false, $data['jetpackAiSidebar']['features']['blockToolbarButton'] ); // generateFeedback and optimizeTitleSuggestion are in development: off outside testing environments. $this->assertSame( false, $data['jetpackAiSidebar']['features']['generateFeedback'] ); $this->assertSame( false, $data['jetpackAiSidebar']['features']['optimizeTitleSuggestion'] ); @@ -628,6 +793,7 @@ public function test_add_agents_manager_data_allows_preview_without_ai_editorial $this->assertSame( true, $data['jetpackAiSidebar']['enabled'] ); $this->assertSame( false, $data['jetpackAiSidebar']['features']['aiEditorialReview'] ); $this->assertSame( true, $data['jetpackAiSidebar']['features']['blockTransformations'] ); + $this->assertSame( false, $data['jetpackAiSidebar']['features']['blockToolbarButton'] ); } /**