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' && ( -