From 2f6d8dffbdecf5cdec11e3fbbea5526bba040683 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 17 Feb 2026 11:34:18 +0100 Subject: [PATCH 1/4] Commands: Add category property to command registration Introduce a `category` field for commands registered via the Command Palette. Categories describe the nature of a command's action: - `command`: executes code or toggles a setting - `view`: navigates to an admin area or opens a panel - `edit`: navigates to edit a document or field - `action`: generic fallback (also the default for invalid values) The `workflow` category is defined in the type system but reserved for a future dedicated API and cannot be registered via `registerCommand`. Invalid categories trigger a dev-mode warning via `@wordpress/warning` and default to `action`. All existing core commands have been annotated with the appropriate category. Co-Authored-By: Claude Opus 4.6 --- .../components/use-block-commands/index.js | 2 + packages/commands/README.md | 16 ++++++ packages/commands/package.json | 1 + packages/commands/src/hooks/use-command.js | 6 +++ packages/commands/src/store/actions.js | 54 ++++++++++++++++--- packages/commands/src/store/reducer.js | 1 + .../src/admin-navigation-commands.js | 2 + .../src/site-editor-navigation-commands.js | 8 +++ .../edit-post/src/commands/use-commands.js | 1 + .../editor/src/components/commands/index.js | 17 ++++++ 10 files changed, 100 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index b154aae8f8c4cd..e0f8f134b23a72 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -136,6 +136,7 @@ const getTransformCommands = () => /* translators: %s: Block or block variation name. */ label: sprintf( __( 'Transform to %s' ), title ), icon: blockIcon?.src, + category: 'command', callback: ( { close } ) => { onBlockTransform( name ); close(); @@ -326,6 +327,7 @@ const getQuickActionsCommands = () => commands: commands.map( ( command ) => ( { ...command, name: 'core/block-editor/action-' + command.name, + category: 'command', callback: ( { close } ) => { command.callback(); close(); diff --git a/packages/commands/README.md b/packages/commands/README.md index 105d42cc3fabf4..1ec8afb44d43d6 100644 --- a/packages/commands/README.md +++ b/packages/commands/README.md @@ -10,6 +10,7 @@ There are two ways to register commands: static or dynamic. Both methods receive - `label`: A human-readable label - `icon`: An SVG icon - `callback`: A callback function that is called when the command is selected +- `category`: (Optional) The category of the command. See [Command categories](#command-categories) below - `context`: (Optional) The context of the command - `keywords`: (Optional) An array of keywords for search matching @@ -37,6 +38,17 @@ As the usage of the Command Palette expands, more contexts will be added. Attaching a command or command loader to a given context is as simple as adding the `context` property (with the right context value from the available contexts above) to the `useCommand` or `useCommandLoader` calls. +## Command categories + +Each command can be assigned a `category` that describes what kind of action it performs. Categories are used by the Command Palette to visually differentiate commands. The following categories are available: + +- `command`: Executes code or toggles a command (e.g., Add block, duplicating a block). +- `view`: Navigates to an area in the admin or opens a panel (e.g., "Go to: Templates"). +- `edit`: Navigates to edit a document (e.g., editing a template, editing a page). +- `action`: A generic fallback for commands that don't fit the other categories. This is also the default when an invalid category is provided. + +If no `category` is specified, the command will have `action` set. If an invalid value is provided, a warning is emitted in development mode and the category defaults to `action`. + ## WordPress Data API The Command Palette also offers a number of [selectors and actions](https://developer.wordpress.org/block-editor/reference-guides/data/data-core-commands/) to manipulate its state, which include: @@ -104,6 +116,7 @@ useCommand( { name: 'myplugin/my-command-name', label: __( 'Add new post' ), icon: plus, + category: 'command', callback: ( { close } ) => { document.location.href = 'post-new.php'; close(); @@ -160,6 +173,7 @@ function usePageSearchCommandLoader( { search } ) { ? record.title?.rendered : __( '(no title)' ), icon: page, + category: 'edit', callback: ( { close } ) => { const args = { p: '/page', @@ -203,6 +217,7 @@ useCommands( [ name: 'myplugin/add-post', label: __( 'Add new post' ), icon: plus, + category: 'command', callback: ( { close } ) => { document.location.href = 'post-new.php'; close(); @@ -212,6 +227,7 @@ useCommands( [ name: 'myplugin/edit-posts', label: __( 'Edit posts' ), icon: pencil, + category: 'view', callback: ( { close } ) => { document.location.href = 'edit.php'; close(); diff --git a/packages/commands/package.json b/packages/commands/package.json index 751c607df82f75..bdf3322cbad544 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -52,6 +52,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/warning": "file:../warning", "clsx": "^2.1.1", "cmdk": "^1.0.0" }, diff --git a/packages/commands/src/hooks/use-command.js b/packages/commands/src/hooks/use-command.js index e3684ce92e8f57..6275284982235e 100644 --- a/packages/commands/src/hooks/use-command.js +++ b/packages/commands/src/hooks/use-command.js @@ -23,6 +23,7 @@ import { store as commandsStore } from '../store'; * name: 'myplugin/my-command-name', * label: __( 'Add new post' ), * icon: plus, + * category: 'command', * callback: ({ close }) => { * document.location.href = 'post-new.php'; * close(); @@ -44,6 +45,7 @@ export function useCommand( command ) { registerCommand( { name: command.name, context: command.context, + category: command.category, label: command.label, searchLabel: command.searchLabel, icon: command.icon, @@ -59,6 +61,7 @@ export function useCommand( command ) { command.searchLabel, command.icon, command.context, + command.category, command.keywords, command.disabled, registerCommand, @@ -81,6 +84,7 @@ export function useCommand( command ) { * name: 'myplugin/add-post', * label: __( 'Add new post' ), * icon: plus, + * category: 'command', * callback: ({ close }) => { * document.location.href = 'post-new.php'; * close(); @@ -90,6 +94,7 @@ export function useCommand( command ) { * name: 'myplugin/edit-posts', * label: __( 'Edit posts' ), * icon: pencil, + * category: 'view', * callback: ({ close }) => { * document.location.href = 'edit.php'; * close(); @@ -124,6 +129,7 @@ export function useCommands( commands ) { registerCommand( { name: command.name, context: command.context, + category: command.category, label: command.label, searchLabel: command.searchLabel, icon: command.icon, diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index bdb7ddaa101edd..2a35d1b75d2a74 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -1,18 +1,42 @@ /** @typedef {import('@wordpress/keycodes').WPKeycodeModifier} WPKeycodeModifier */ +/** + * WordPress dependencies + */ +import warning from '@wordpress/warning'; + +/** + * @typedef {'command'|'view'|'edit'|'workflow'|'action'} WPCommandCategory + */ + +/** + * Command categories allowed via registerCommand. + * The 'workflow' category is reserved for internal use + * and cannot be registered through this API. + * + * @type {Set} + */ +const REGISTERABLE_CATEGORIES = new Set( [ + 'command', + 'view', + 'edit', + 'action', +] ); + /** * Configuration of a registered keyboard shortcut. * * @typedef {Object} WPCommandConfig * - * @property {string} name Command name. - * @property {string} label Command label. - * @property {string=} searchLabel Command search label. - * @property {string=} context Command context. - * @property {React.JSX.Element} icon Command icon. - * @property {Function} callback Command callback. - * @property {boolean} disabled Whether to disable the command. - * @property {string[]=} keywords Command keywords for search matching. + * @property {string} name Command name. + * @property {string} label Command label. + * @property {string=} searchLabel Command search label. + * @property {string=} context Command context. + * @property {WPCommandCategory=} category Command category. + * @property {React.JSX.Element} icon Command icon. + * @property {Function} callback Command callback. + * @property {boolean} disabled Whether to disable the command. + * @property {string[]=} keywords Command keywords for search matching. */ /** @@ -38,9 +62,23 @@ * @return {Object} action. */ export function registerCommand( config ) { + let { category } = config; + + if ( ! category || ! REGISTERABLE_CATEGORIES.has( category ) ) { + warning( + 'Command "' + + config.name + + '" has invalid category "' + + category + + '". Defaulting to "action".' + ); + category = 'action'; + } + return { type: 'REGISTER_COMMAND', ...config, + category, }; } diff --git a/packages/commands/src/store/reducer.js b/packages/commands/src/store/reducer.js index 568ca5b0eaf43a..fa0a69a8cf5dd1 100644 --- a/packages/commands/src/store/reducer.js +++ b/packages/commands/src/store/reducer.js @@ -21,6 +21,7 @@ function commands( state = {}, action ) { label: action.label, searchLabel: action.searchLabel, context: action.context, + category: action.category, callback: action.callback, icon: action.icon, keywords: action.keywords, diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index 808e75422baccc..171477f01ccfa6 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -28,6 +28,7 @@ const getViewSiteCommand = () => name: 'core/view-site', label: __( 'View site' ), icon: external, + category: 'view', callback: ( { close } ) => { close(); window.open( homeUrl, '_blank' ); @@ -54,6 +55,7 @@ export function useAdminNavigationCommands( menuCommands ) { name: menuCommand.name, label, searchLabel: label, + category: 'view', callback: ( { close } ) => { document.location = menuCommand.url; close(); diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index c6e0e4e5dc0328..da6a3c2915631c 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -152,6 +152,7 @@ const getNavigationCommandLoaderPerPostType = ( postType ) => ? decodeEntities( record.title?.rendered ) : __( '(no title)' ), icon: icons[ postType ], + category: 'edit', }; if ( @@ -260,6 +261,7 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => ? record.title?.rendered : __( '(no title)' ), icon: icons[ templateType ], + category: 'edit', callback: ( { close } ) => { if ( isSiteEditor ) { history.navigate( @@ -288,6 +290,7 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => name: 'core/edit-site/open-template-parts', label: __( 'Go to: Template parts' ), icon: symbolFilled, + category: 'view', callback: ( { close } ) => { if ( isSiteEditor ) { history.navigate( @@ -346,6 +349,7 @@ const getSiteEditorBasicNavigationCommands = () => name: 'core/edit-site/open-styles', label: __( 'Go to: Styles' ), icon: styles, + category: 'view', callback: ( { close } ) => { if ( isSiteEditor ) { history.navigate( '/styles' ); @@ -365,6 +369,7 @@ const getSiteEditorBasicNavigationCommands = () => name: 'core/edit-site/open-navigation', label: __( 'Go to: Navigation' ), icon: navigation, + category: 'view', callback: ( { close } ) => { if ( isSiteEditor ) { history.navigate( '/navigation' ); @@ -384,6 +389,7 @@ const getSiteEditorBasicNavigationCommands = () => name: 'core/edit-site/open-templates', label: __( 'Go to: Templates' ), icon: layout, + category: 'view', callback: ( { close } ) => { if ( isSiteEditor ) { history.navigate( mapRoute( '/template' ) ); @@ -405,6 +411,7 @@ const getSiteEditorBasicNavigationCommands = () => name: 'core/edit-site/open-patterns', label: __( 'Go to: Patterns' ), icon: symbol, + category: 'view', callback: ( { close } ) => { if ( canCreateTemplate ) { if ( isSiteEditor ) { @@ -470,6 +477,7 @@ const getGlobalStylesOpenCssCommands = () => name: 'core/open-styles-css', label: __( 'Open custom CSS' ), icon: brush, + category: 'view', callback: ( { close } ) => { close(); diff --git a/packages/edit-post/src/commands/use-commands.js b/packages/edit-post/src/commands/use-commands.js index edd2a6a5b84042..acd53411f89c38 100644 --- a/packages/edit-post/src/commands/use-commands.js +++ b/packages/edit-post/src/commands/use-commands.js @@ -25,6 +25,7 @@ export default function useCommands() { ? __( 'Exit fullscreen' ) : __( 'Enter fullscreen' ), icon: fullscreen, + category: 'command', callback: ( { close } ) => { toggle( 'core/edit-post', 'fullscreenMode' ); close(); diff --git a/packages/editor/src/components/commands/index.js b/packages/editor/src/components/commands/index.js index 907367e8a1cad9..cd57ab6bc0ae61 100644 --- a/packages/editor/src/components/commands/index.js +++ b/packages/editor/src/components/commands/index.js @@ -103,6 +103,7 @@ const getEditorCommandLoader = () => name: 'core/open-shortcut-help', label: __( 'Keyboard shortcuts' ), icon: keyboard, + category: 'view', callback: ( { close } ) => { close(); openModal( 'editor/keyboard-shortcut-help' ); @@ -114,6 +115,7 @@ const getEditorCommandLoader = () => label: isDistractionFree ? __( 'Exit Distraction free' ) : __( 'Enter Distraction free' ), + category: 'command', callback: ( { close } ) => { toggleDistractionFree(); close(); @@ -123,6 +125,7 @@ const getEditorCommandLoader = () => commands.push( { name: 'core/open-preferences', label: __( 'Editor preferences' ), + category: 'view', callback: ( { close } ) => { close(); openModal( 'editor/preferences' ); @@ -134,6 +137,7 @@ const getEditorCommandLoader = () => label: isFocusMode ? __( 'Exit Spotlight mode' ) : __( 'Enter Spotlight mode' ), + category: 'command', callback: ( { close } ) => { toggleSpotlightMode(); close(); @@ -146,6 +150,7 @@ const getEditorCommandLoader = () => ? __( 'Close List View' ) : __( 'Open List View' ), icon: listView, + category: 'command', callback: ( { close } ) => { setIsListViewOpened( ! isListViewOpen ); close(); @@ -164,6 +169,7 @@ const getEditorCommandLoader = () => commands.push( { name: 'core/toggle-top-toolbar', label: __( 'Top toolbar' ), + category: 'command', callback: ( { close } ) => { toggleTopToolbar(); close(); @@ -178,6 +184,7 @@ const getEditorCommandLoader = () => ? __( 'Open code editor' ) : __( 'Exit code editor' ), icon: code, + category: 'command', callback: ( { close } ) => { switchEditorMode( editorMode === 'visual' ? 'text' : 'visual' @@ -192,6 +199,7 @@ const getEditorCommandLoader = () => label: showBlockBreadcrumbs ? __( 'Hide block breadcrumbs' ) : __( 'Show block breadcrumbs' ), + category: 'command', callback: ( { close } ) => { toggle( 'core', 'showBlockBreadcrumbs' ); close(); @@ -211,6 +219,7 @@ const getEditorCommandLoader = () => name: 'core/open-settings-sidebar', label: __( 'Show or hide the Settings panel' ), icon: isRTL() ? drawerLeft : drawerRight, + category: 'command', callback: ( { close } ) => { const activeSidebar = getActiveComplementaryArea( 'core' ); close(); @@ -226,6 +235,7 @@ const getEditorCommandLoader = () => name: 'core/open-block-inspector', label: __( 'Show or hide the Block settings panel' ), icon: blockDefault, + category: 'command', callback: ( { close } ) => { const activeSidebar = getActiveComplementaryArea( 'core' ); close(); @@ -243,6 +253,7 @@ const getEditorCommandLoader = () => ? __( 'Disable pre-publish checks' ) : __( 'Enable pre-publish checks' ), icon: formatListBullets, + category: 'command', callback: ( { close } ) => { close(); toggle( 'core', 'isPublishSidebarEnabled' ); @@ -263,6 +274,7 @@ const getEditorCommandLoader = () => name: 'core/preview-link', label: __( 'Preview in a new tab' ), icon: external, + category: 'view', callback: async ( { close } ) => { close(); const postId = getCurrentPostId(); @@ -294,6 +306,7 @@ const getEditedEntityContextualCommands = () => name: 'core/rename-pattern', label: __( 'Rename pattern' ), icon: pencil, + category: 'edit', callback: ( { close } ) => { openModal( patternRenameModalName ); close(); @@ -303,6 +316,7 @@ const getEditedEntityContextualCommands = () => name: 'core/duplicate-pattern', label: __( 'Duplicate pattern' ), icon: symbol, + category: 'command', callback: ( { close } ) => { openModal( patternDuplicateModalName ); close(); @@ -367,6 +381,7 @@ const getPageContentFocusCommands = () => decodeEntities( template.title ) ), icon: layout, + category: 'edit', callback: ( { close } ) => { onNavigateToEntityRecord( { postId: templateId, @@ -382,6 +397,7 @@ const getPageContentFocusCommands = () => name: 'core/switch-to-previous-entity', label: __( 'Go back' ), icon: page, + category: 'view', callback: ( { close } ) => { goBack(); close(); @@ -438,6 +454,7 @@ const getManipulateDocumentCommands = () => name: 'core/reset-template', label, icon: isRTL() ? rotateRight : rotateLeft, + category: 'command', callback: ( { close } ) => { revertTemplate( template ); close(); From e929f4ca6fbb578145d5ff6948217972c3aeffd5 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 17 Feb 2026 12:25:34 +0100 Subject: [PATCH 2/4] Commands: Update package-lock and add category to command loader example Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 1 + packages/commands/src/hooks/use-command-loader.js | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index 00c6da7e572675..5b7cf36fa43f0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58029,6 +58029,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/warning": "file:../warning", "clsx": "^2.1.1", "cmdk": "^1.0.0" }, diff --git a/packages/commands/src/hooks/use-command-loader.js b/packages/commands/src/hooks/use-command-loader.js index d8dace05b46e7c..006114b3a4b27d 100644 --- a/packages/commands/src/hooks/use-command-loader.js +++ b/packages/commands/src/hooks/use-command-loader.js @@ -54,6 +54,7 @@ import { store as commandsStore } from '../store'; * ? record.title?.rendered * : __( '(no title)' ), * icon: page, + * category: 'edit', * callback: ( { close } ) => { * const args = { * p: '/page', From 510c38582e2ec1225191e325eb2d1e08377e9030 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 17 Feb 2026 12:27:20 +0100 Subject: [PATCH 3/4] add changelog --- packages/commands/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index 66dab7271354b1..b184588da3643f 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add `category` property to command config, used to visually differentiate commands in the Command Palette. + ## 1.39.0 (2026-01-29) ## 1.38.0 (2026-01-16) From 85046ef84d1daf1bac6e15e78ec587df5489d0e4 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Tue, 17 Feb 2026 12:29:59 +0100 Subject: [PATCH 4/4] remove console warning --- packages/commands/src/store/actions.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index 2a35d1b75d2a74..c66a19f9589da0 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -1,10 +1,5 @@ /** @typedef {import('@wordpress/keycodes').WPKeycodeModifier} WPKeycodeModifier */ -/** - * WordPress dependencies - */ -import warning from '@wordpress/warning'; - /** * @typedef {'command'|'view'|'edit'|'workflow'|'action'} WPCommandCategory */ @@ -64,14 +59,8 @@ const REGISTERABLE_CATEGORIES = new Set( [ export function registerCommand( config ) { let { category } = config; + // Defaults to 'action' if no category is provided or if the category is invalid. Future versions will emit a warning. if ( ! category || ! REGISTERABLE_CATEGORIES.has( category ) ) { - warning( - 'Command "' + - config.name + - '" has invalid category "' + - category + - '". Defaulting to "action".' - ); category = 'action'; }