diff --git a/projects/packages/my-jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes b/projects/packages/my-jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes new file mode 100644 index 000000000000..8fdc17645c8f --- /dev/null +++ b/projects/packages/my-jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fix PHP deprecated notice: cast $tier to int before using as array offset in get_long_description_by_usage_tier() to handle null from get_next_usage_tier() diff --git a/projects/packages/my-jetpack/src/products/class-jetpack-ai.php b/projects/packages/my-jetpack/src/products/class-jetpack-ai.php index e52030dc34e4..0bbe62df8ce6 100644 --- a/projects/packages/my-jetpack/src/products/class-jetpack-ai.php +++ b/projects/packages/my-jetpack/src/products/class-jetpack-ai.php @@ -234,17 +234,18 @@ public static function get_description() { /** * Get the internationalized usage tier long description by tier * - * @param int $tier The usage tier. + * @param int|null $tier The usage tier. * @return string */ public static function get_long_description_by_usage_tier( $tier ) { - $long_descriptions = array( - 1 => __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' ), - 100 => __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' ), - ); - $tiered_description = __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' ); - - return isset( $long_descriptions[ $tier ] ) ? $long_descriptions[ $tier ] : $tiered_description; + switch ( (int) $tier ) { + case 1: + return __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' ); + case 100: + return __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' ); + default: + return __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' ); + } } /** diff --git a/projects/plugins/jetpack/_inc/client/ai/main.jsx b/projects/plugins/jetpack/_inc/client/ai/main.jsx index 8ea22363e122..1c11f0749dc6 100644 --- a/projects/plugins/jetpack/_inc/client/ai/main.jsx +++ b/projects/plugins/jetpack/_inc/client/ai/main.jsx @@ -19,6 +19,13 @@ import McpWrite from './mcp/write'; const { blogId, activityLogUrl, apiRoot, apiNonce } = window?.jetpackAiSettings ?? {}; +const VALID_VIEWS = [ 'read', 'write', 'setup' ]; + +const getViewFromHash = () => { + const hash = window.location.hash.replace( /^#\//, '' ); + return VALID_VIEWS.includes( hash ) ? hash : 'hub'; +}; + const VIEW_TITLES = { hub: 'AI', // "AI" is a product name and should not be translated. read: __( 'Read', 'jetpack' ), @@ -71,11 +78,19 @@ function Breadcrumbs( { view, onNavigate } ) { * @return {object} Component markup. */ export default function App() { - const [ view, setView ] = useState( 'hub' ); + const [ view, setView ] = useState( getViewFromHash ); const [ saveError, setSaveError ] = useState( null ); const { isLoading, savingToolIds, mcpAbilities, hasMcpAccess, error, updateMcpAbilities } = useMcpSettings(); + useEffect( () => { + // Tag the initial history entry so the popstate handler can restore the hub view. + window.history.replaceState( { view: getViewFromHash() }, '' ); + const handlePopState = event => setView( event.state?.view ?? 'hub' ); + window.addEventListener( 'popstate', handlePopState ); + return () => window.removeEventListener( 'popstate', handlePopState ); + }, [] ); + useEffect( () => { if ( ! isLoading && hasMcpAccess ) { analytics.tracks.recordEvent( 'jetpack_mcp_settings_viewed' ); @@ -93,7 +108,15 @@ export default function App() { ); const dismissSaveError = useCallback( () => setSaveError( null ), [] ); - const navigateBack = useCallback( () => setView( 'hub' ), [] ); + + const navigateToView = useCallback( newView => { + window.history.pushState( { view: newView }, '', '#/' + newView ); + setView( newView ); + }, [] ); + + // The breadcrumb back link mirrors the browser Back button so the history + // entry for the sub-view is popped rather than a new hub entry being pushed. + const navigateBack = useCallback( () => window.history.back(), [] ); const isSubView = view !== 'hub'; @@ -148,7 +171,7 @@ export default function App() { blogId={ blogId } activityLogUrl={ activityLogUrl } savingToolIds={ savingToolIds } - onNavigate={ setView } + onNavigate={ navigateToView } onUpdate={ handleUpdate } /> ) } diff --git a/projects/plugins/jetpack/_inc/client/ai/mcp/categories.js b/projects/plugins/jetpack/_inc/client/ai/mcp/categories.js index 8d78bcf01d2c..77c54884d443 100644 --- a/projects/plugins/jetpack/_inc/client/ai/mcp/categories.js +++ b/projects/plugins/jetpack/_inc/client/ai/mcp/categories.js @@ -30,15 +30,26 @@ export const CATEGORY_ORDER = [ ]; const SUB_CATEGORIES = { + // Posts sub-categories POSTS: __( 'Posts', 'jetpack' ), COMMENTS: __( 'Comments', 'jetpack' ), CATEGORIES_TAGS: __( 'Categories & tags', 'jetpack' ), + // Sites sub-categories SITES: __( 'Sites', 'jetpack' ), + PLUGINS: __( 'Plugins', 'jetpack' ), MEDIA: __( 'Media', 'jetpack' ), SITE_SETTINGS: __( 'Site settings', 'jetpack' ), ANALYTICS: __( 'Analytics', 'jetpack' ), + // Account sub-categories ACCOUNT: __( 'Account', 'jetpack' ), NOTIFICATIONS: __( 'Notifications', 'jetpack' ), + // Design sub-categories + THEMES: __( 'Themes', 'jetpack' ), + PATTERNS: __( 'Patterns', 'jetpack' ), + TEMPLATES: __( 'Templates', 'jetpack' ), + GLOBAL_STYLES: __( 'Global styles', 'jetpack' ), + NAVIGATION: __( 'Navigation', 'jetpack' ), + BLOCKS: __( 'Blocks', 'jetpack' ), }; export const SUB_CATEGORY_ORDER = { @@ -49,11 +60,20 @@ export const SUB_CATEGORY_ORDER = { ], [ DISPLAY_CATEGORIES.SITES ]: [ SUB_CATEGORIES.SITES, + SUB_CATEGORIES.PLUGINS, SUB_CATEGORIES.SITE_SETTINGS, SUB_CATEGORIES.MEDIA, SUB_CATEGORIES.ANALYTICS, ], [ DISPLAY_CATEGORIES.ACCOUNT ]: [ SUB_CATEGORIES.ACCOUNT, SUB_CATEGORIES.NOTIFICATIONS ], + [ DISPLAY_CATEGORIES.DESIGN ]: [ + SUB_CATEGORIES.THEMES, + SUB_CATEGORIES.PATTERNS, + SUB_CATEGORIES.TEMPLATES, + SUB_CATEGORIES.GLOBAL_STYLES, + SUB_CATEGORIES.NAVIGATION, + SUB_CATEGORIES.BLOCKS, + ], }; const API_CATEGORY_TO_DISPLAY = { @@ -76,20 +96,40 @@ const API_CATEGORY_TO_DISPLAY = { }; const API_CATEGORY_TO_SUB_CATEGORY = { + // Posts card sub-categories posts: SUB_CATEGORIES.POSTS, comments: SUB_CATEGORIES.COMMENTS, 'categories-tags': SUB_CATEGORIES.CATEGORIES_TAGS, + // Sites card sub-categories sites: SUB_CATEGORIES.SITES, media: SUB_CATEGORIES.MEDIA, users: SUB_CATEGORIES.SITE_SETTINGS, - plugins: SUB_CATEGORIES.SITE_SETTINGS, + plugins: SUB_CATEGORIES.PLUGINS, 'site-settings': SUB_CATEGORIES.SITE_SETTINGS, analytics: SUB_CATEGORIES.ANALYTICS, + // Account card sub-categories account: SUB_CATEGORIES.ACCOUNT, notifications: SUB_CATEGORIES.NOTIFICATIONS, billing: SUB_CATEGORIES.ACCOUNT, }; +// Design-card tools all share `design` as their API category, so sub-groups within the +// Design card are derived from tool ID prefixes. This also covers `sites`-category tools +// that are routed to the Design card (navigation, menus, themes). +const TOOL_ID_PREFIX_TO_DESIGN_SUB_CATEGORY = { + 'wpcom-mcp/theme-': SUB_CATEGORIES.THEMES, + 'wpcom-mcp/themes-': SUB_CATEGORIES.THEMES, + 'wpcom-mcp/patterns-': SUB_CATEGORIES.PATTERNS, + 'wpcom-mcp/synced-patterns-': SUB_CATEGORIES.PATTERNS, + 'wpcom-mcp/templates-': SUB_CATEGORIES.TEMPLATES, + 'wpcom-mcp/template-parts-': SUB_CATEGORIES.TEMPLATES, + 'wpcom-mcp/global-styles-': SUB_CATEGORIES.GLOBAL_STYLES, + 'wpcom-mcp/navigation-': SUB_CATEGORIES.NAVIGATION, + 'wpcom-mcp/menus-': SUB_CATEGORIES.NAVIGATION, + 'wpcom-mcp/menu-items-': SUB_CATEGORIES.NAVIGATION, + 'wpcom-mcp/blocks-': SUB_CATEGORIES.BLOCKS, +}; + /** * Get the display sub-category name for a tool. * @@ -99,6 +139,24 @@ const API_CATEGORY_TO_SUB_CATEGORY = { */ export function getSubCategory( toolId, ability ) { const apiCategory = ability?.category; + + // Design-card tools use tool ID prefix for sub-grouping. This covers both + // 'design'-category tools and 'sites'-category tools that are routed to the + // Design card by getDisplayCategory (e.g. navigation, menus, themes). + if ( apiCategory === 'design' || apiCategory === 'sites' ) { + for ( const [ prefix, subCategory ] of Object.entries( + TOOL_ID_PREFIX_TO_DESIGN_SUB_CATEGORY + ) ) { + if ( toolId.startsWith( prefix ) ) { + return subCategory; + } + } + // Tools in these API categories should still be rendered even when their + // IDs do not match a known Design prefix, so fall back to the category's + // default sub-category instead of returning undefined. + return API_CATEGORY_TO_SUB_CATEGORY[ apiCategory ]; + } + if ( apiCategory ) { return API_CATEGORY_TO_SUB_CATEGORY[ apiCategory ]; } @@ -119,12 +177,24 @@ export function isWriteTool( toolId, ability ) { /** * Get the display category name for a tool. * + * For 'design' and 'sites' category tools, check if the tool ID prefix indicates + * it belongs in the Design card (navigation, menus, themes mirror calypso's approach). + * * @param {string} toolId - Tool identifier. * @param {object} ability - Tool descriptor from the API. * @return {string} Display category name, falling back to Uncategorized. */ export function getDisplayCategory( toolId, ability ) { const apiCategory = ability?.category; + + if ( apiCategory === 'design' || apiCategory === 'sites' ) { + for ( const prefix of Object.keys( TOOL_ID_PREFIX_TO_DESIGN_SUB_CATEGORY ) ) { + if ( toolId.startsWith( prefix ) ) { + return DISPLAY_CATEGORIES.DESIGN; + } + } + } + if ( apiCategory && API_CATEGORY_TO_DISPLAY[ apiCategory ] ) { return API_CATEGORY_TO_DISPLAY[ apiCategory ]; } diff --git a/projects/plugins/jetpack/_inc/client/ai/mcp/setup.jsx b/projects/plugins/jetpack/_inc/client/ai/mcp/setup.jsx index 265dd758bcdc..aa3959b17f74 100644 --- a/projects/plugins/jetpack/_inc/client/ai/mcp/setup.jsx +++ b/projects/plugins/jetpack/_inc/client/ai/mcp/setup.jsx @@ -126,7 +126,7 @@ export default function McpSetup() { { selectedClient === 'claude' && ( -
    +
+ ) } { selectedClient === 'claude-code' && ( @@ -162,7 +162,7 @@ export default function McpSetup() { 'jetpack' ) } -
    +
+ ) } diff --git a/projects/plugins/jetpack/_inc/client/ai/mcp/style.scss b/projects/plugins/jetpack/_inc/client/ai/mcp/style.scss index 0a2fa21f645d..e0d89183a04d 100644 --- a/projects/plugins/jetpack/_inc/client/ai/mcp/style.scss +++ b/projects/plugins/jetpack/_inc/client/ai/mcp/style.scss @@ -83,6 +83,7 @@ display: inline-flex; align-items: center; gap: 4px; + vertical-align: center; } &__category-header-row { @@ -91,17 +92,37 @@ justify-content: space-between; align-items: center; width: 100%; + + // The ToggleControl label uses line-height equal to the toggle height for + // vertical alignment, but the FlexBlock wrapper defaults to align-items: + // stretch. Force center so the "Enable all" text sits mid-toggle. + .components-toggle-control__label { + display: flex; + align-items: center; + } } &__tool-group-divider { margin: 0; } + // Tighten the gap between each tool toggle's help text and the next toggle. + // WP's BaseControl adds margin-top: 8px on the help text; bump overall + // row gap down slightly so the combined visual spacing feels even. + .components-base-control__help { + margin-top: 4px; + } + &__access-card { overflow: hidden; } } +// wp-admin resets list-style on ul; restore bullets for the steps list. +ul.jetpack-ai-mcp-setup__steps { + list-style: disc; +} + .jetpack-ai-mcp-setup { &__steps { diff --git a/projects/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-ai-page.php b/projects/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-ai-page.php index 8f4a28568c27..ffe54d390c40 100644 --- a/projects/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-ai-page.php +++ b/projects/plugins/jetpack/_inc/lib/admin-pages/class-jetpack-ai-page.php @@ -10,7 +10,6 @@ use Automattic\Jetpack\Admin_UI\Admin_Menu; use Automattic\Jetpack\Connection\Manager as Connection_Manager; -use Automattic\Jetpack\Redirect; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; @@ -86,13 +85,12 @@ public function page_admin_scripts() { // Use the plain hostname for the Atomic activity log URL — get_site_suffix() can // include '::' for subdirectory installs, which would break the URL. This matches // the approach used by jetpack-mu-wpcom for the sidebar Activity Log link. - $site_host = wp_parse_url( home_url(), PHP_URL_HOST ); - // On Atomic, jetpack-mu-wpcom replaces the cloud redirect with a direct WPCOM URL. - // Mirror that behaviour here so the activity log link resolves consistently. + $site_host = wp_parse_url( home_url(), PHP_URL_HOST ); $activity_log_site = ( is_string( $site_host ) && '' !== $site_host ) ? $site_host : $site_suffix; - $activity_log_url = ( new Host() )->is_woa_site() + // On Atomic link to WPCOM activity log; on self-hosted link to the local wp-admin page. + $activity_log_url = ( new Host() )->is_woa_site() ? 'https://wordpress.com/activity-log/' . $activity_log_site - : Redirect::get_url( 'cloud-activity-log-wp-menu', array( 'site' => $blog_id ? $blog_id : $site_suffix ) ); + : admin_url( 'admin.php?page=jetpack-activity-log' ); wp_enqueue_script( 'jetpack-ai-admin', diff --git a/projects/plugins/jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes b/projects/plugins/jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes new file mode 100644 index 000000000000..1f0d21ebebb4 --- /dev/null +++ b/projects/plugins/jetpack/changelog/aiint-392-jetpack-ai-mcp-settings-ui-polish-fixes @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Improved the MCP settings UI by fixing browser Back button navigation, changing setup instructions to unordered lists, correcting the 'All enabled' label vertical alignment, and tightening toggle help-text spacing.