Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Jetpack AI: Hide the legacy AI panel once a site is eligible to turn on the WordPress Agent, not only once it is switched on. The notice in its place offers to open the Agent, or to enable it where it is not yet on.
1 change: 1 addition & 0 deletions projects/plugins/jetpack/extensions/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"ai-assistant-image-extension",
"ai-sidebar-toolbar-button",
"ai-sidebar-agent-notice",
"ai-sidebar-agent-enabled",
"ai-seo-enhancer",
"ai-correct-spelling",
"paypal-payment-buttons",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use Automattic\Jetpack\Agents_Manager\Agents_Manager;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\SEO\Ai_Seo;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host;
Expand All @@ -32,6 +33,7 @@
const AI_SIDEBAR_AGENT_ID = 'wp-orchestrator';
const AI_SIDEBAR_TOOLBAR_BUTTON_EXTENSION = 'ai-sidebar-toolbar-button';
const AI_SIDEBAR_AGENT_NOTICE_EXTENSION = 'ai-sidebar-agent-notice';
const AI_SIDEBAR_AGENT_ENABLED_EXTENSION = 'ai-sidebar-agent-enabled';

/**
* Initializes the Agents Manager package and registers the Jetpack AI
Expand All @@ -45,6 +47,9 @@ class Jetpack_AI_Sidebar {
* @return void
*/
public static function init(): void {
// Ahead of the sidebar gate: the notice also shows on sites merely eligible for the Agent.
add_action( 'jetpack_register_gutenberg_extensions', array( __CLASS__, 'register_agent_notice_extension' ), 99 );

// Gate the whole sidebar entrypoint on the preview surface, which is
// itself overridable via the jetpack_ai_sidebar_enabled filter.
if ( ! self::is_jetpack_ai_sidebar_preview_enabled() ) {
Expand Down Expand Up @@ -81,9 +86,6 @@ public static function init(): void {

// 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 );

// Let editor JS know when the legacy AI panel can point people at the WordPress Agent.
add_action( 'jetpack_register_gutenberg_extensions', array( __CLASS__, 'register_agent_notice_extension' ), 99 );
}

// ──────────────────────────────────────────────────
Expand Down Expand Up @@ -454,8 +456,10 @@ private static function get_jetpack_ai_sidebar_preview_config(): array {
$features['blockTransformations'] = (bool) $features['blockTransformations'] && $writing_on;

return array(
'enabled' => self::is_jetpack_ai_sidebar_preview_enabled(),
'features' => $features,
'enabled' => self::is_jetpack_ai_sidebar_preview_enabled(),
'features' => $features,
// Not in $features, which is public and host-filterable.
'agentNoticeActionAvailable' => self::is_agent_action_available(),
);
}

Expand Down Expand Up @@ -498,13 +502,67 @@ public static function register_toolbar_button_extension(): void {
/**
* Whether the legacy AI panel should point people at the WordPress Agent.
*
* The notice replaces the panel, so it needs an agent to send people to. A
* disconnected user gets the Agents Manager's reduced build, which has no chat.
* True once there is a working agent to open, or earlier, once the site is
* eligible to turn one on.
*
* @return bool
*/
public static function is_agent_notice_enabled(): bool {
return self::should_expose_provider() && ! self::is_agents_manager_disconnected();
if ( self::is_agent_action_available() ) {
return true;
}

return self::is_wordpress_agent_eligible()
&& self::is_supported_provider_surface()
&& self::has_ai_features()
&& ! self::is_agents_manager_disconnected();
}

/**
* Whether the site has turned the WordPress Agent on, whatever Jetpack's own
* sidebar gate says.
*
* The Big Sky plugin's own Settings > Writing checkbox can turn it off while
* the plugin stays active, so the option counts as well as the class.
*
* @return bool
*/
private static function is_wordpress_agent_enabled(): bool {
$host = new Host();

if ( ! $host->is_wpcom_platform() || ! class_exists( 'Big_Sky' ) ) {
return false;
}

$default = $host->is_wpcom_simple() ? '1' : '0';

return (bool) get_option( 'big_sky_enable', $default );
}

/**
* Whether the site is eligible to turn the WordPress Agent on, whether or not
* it has done so yet.
*
* Prefers the wpcom mu-plugin functions: the plan check cannot see the free
* trial. Not gated on the Big_Sky class, which on Simple only loads once the
* Agent is on.
*
* @return bool
*/
private static function is_wordpress_agent_eligible(): bool {
$host = new Host();

if ( ! $host->is_wpcom_platform() ) {
return false;
}

if ( function_exists( 'big_sky_is_available_for_site' ) && function_exists( 'big_sky_is_enabled' ) ) {
// @phan-suppress-next-line PhanUndeclaredFunction -- Provided by WPCOM's Big Sky mu-plugin; guarded by function_exists() above.
return big_sky_is_available_for_site() || big_sky_is_enabled();
}

// Off Simple the plugin is only installed once the site qualifies.
return class_exists( 'Big_Sky' ) || Current_Plan::supports( 'big-sky' );
}

/**
Expand All @@ -523,6 +581,29 @@ public static function register_agent_notice_extension(): void {
}

\Jetpack_Gutenberg::set_extension_available( AI_SIDEBAR_AGENT_NOTICE_EXTENSION );

// Tells the notice whether to offer to enable the Agent or to open it.
if ( self::is_wordpress_agent_enabled() ) {
\Jetpack_Gutenberg::set_extension_available( AI_SIDEBAR_AGENT_ENABLED_EXTENSION );
return;
}

\Jetpack_Gutenberg::set_extension_unavailable(
AI_SIDEBAR_AGENT_ENABLED_EXTENSION,
'jetpack_ai_sidebar_agent_not_enabled'
);
}

/**
* Whether the notice's action button has a working agent to send people to.
*
* Narrower than is_agent_notice_enabled(): an eligible site shows the notice,
* but only an enabled one has a chat to open.
*
* @return bool
*/
public static function is_agent_action_available(): bool {
return self::should_expose_provider() && ! self::is_agents_manager_disconnected();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ jest.mock( '@wordpress/components', () => ( {
} ) );

jest.mock( '@wordpress/ui', () => ( {
Icon: () => <span data-testid="agent-icon" />,
Button: Object.assign(
( { children, onClick }: { children: React.ReactNode; onClick?: () => void } ) => (
<button onClick={ onClick }>{ children }</button>
Expand All @@ -185,6 +186,9 @@ jest.mock( '@wordpress/ui', () => ( {
Link: ( { children, href }: { children: React.ReactNode; href: string } ) => (
<a href={ href }>{ children }</a>
),
LinkButton: ( { children, href }: { children: React.ReactNode; href: string } ) => (
<a href={ href }>{ children }</a>
),
Notice: {
Root: ( { children }: { children: React.ReactNode } ) => <div>{ children }</div>,
Description: ( { children }: { children: React.ReactNode } ) => <span>{ children }</span>,
Expand Down Expand Up @@ -277,13 +281,17 @@ describe( 'AiAssistantPluginSidebar', () => {

afterEach( () => {
delete ( window as unknown as { __agentsManagerActions?: unknown } ).__agentsManagerActions;
delete ( window as unknown as { agentsManagerData?: unknown } ).agentsManagerData;
} );

describe( 'WordPress Agent notice', () => {
beforeEach( () => {
jest
.mocked( getFeatureAvailability )
.mockImplementation( feature => feature === AGENT_NOTICE_FEATURE );
( window as unknown as { agentsManagerData?: unknown } ).agentsManagerData = {
jetpackAiSidebar: { agentNoticeActionAvailable: true },
};
} );

it( 'shows the notice in the Jetpack sidebar, the document panel and the pre-publish panel', () => {
Expand Down Expand Up @@ -321,7 +329,7 @@ describe( 'AiAssistantPluginSidebar', () => {

expect(
within( screen.getByTestId( 'document-panel' ) ).getByRole( 'button', {
name: 'WordPress Agent',
name: 'Open WordPress Agent',
} )
).toBeInTheDocument();
} );
Expand All @@ -336,7 +344,7 @@ describe( 'AiAssistantPluginSidebar', () => {

await user.click(
within( screen.getByTestId( testId ) ).getByRole( 'button', {
name: 'WordPress Agent',
name: 'Open WordPress Agent',
} )
);

Expand All @@ -346,6 +354,43 @@ describe( 'AiAssistantPluginSidebar', () => {
);
} );

it( 'still replaces the AI panel, without the action, on a site merely eligible for the Agent', () => {
( window as unknown as { agentsManagerData?: unknown } ).agentsManagerData = {
jetpackAiSidebar: { agentNoticeActionAvailable: false },
};

render( <AiAssistantPluginSidebar /> );

expect(
within( screen.getByTestId( 'document-panel' ) ).getByText(
'AI tools have moved to the WordPress Agent.'
)
).toBeInTheDocument();
expect( screen.queryByText( 'Get Feedback' ) ).not.toBeInTheDocument();
expect(
within( screen.getByTestId( 'document-panel' ) ).queryByRole( 'button', {
name: 'Open WordPress Agent',
} )
).not.toBeInTheDocument();
expect(
within( screen.getByTestId( 'document-panel' ) ).getByRole( 'link', {
name: 'Enable WordPress Agent',
} )
).toBeInTheDocument();
} );

it( 'offers to enable the Agent when the server sends no payload at all', () => {
delete ( window as unknown as { agentsManagerData?: unknown } ).agentsManagerData;

render( <AiAssistantPluginSidebar /> );

expect(
within( screen.getByTestId( 'document-panel' ) ).getByRole( 'link', {
name: 'Enable WordPress Agent',
} )
).toBeInTheDocument();
} );

it( 'leaves the collapsed panels alone when there is no notice to show', () => {
withFeatures( DEFAULT_FEATURES );

Expand Down
Loading
Loading