Skip to content

Conversation

emdashcodes
Copy link
Contributor

@emdashcodes emdashcodes commented Sep 11, 2025

This PR follows up on #60 and introduces client-side only abilities. This allows for new integrations that deal with things like UI elements, navigation, and client-side data (such as the block editor). In addition to our AI efforts, it's also meant as a way that the command palette can execute client-side abilities in the future.

It adds two new methods to manage the client-only abilities registerAbility and unregisterAbility.

registerAbility takes the same shape as wp_register_ability in addition to a client side callback function. A callback can take the provided input_schema, do an action, and return the provided output_schema.

Here is a simple example:

registerAbility( {
	name: 'demo/navigate-admin',
	label: 'Navigate to Admin Page',
	description: 'Navigates to a specific WordPress admin page',
	location: 'client',
	meta: {
		type: 'tool',
	},
	input_schema: {
		type: 'object',
		properties: {
			page: {
				type: 'string',
				description: 'The admin page to navigate to (e.g., "users.php", "options-general.php")',
			},
			params: {
				type: 'object',
				description: 'Optional query parameters',
				additionalProperties: true,
			},
		},
		required: [ 'page' ],
	},
	output_schema: {
		type: 'object',
		properties: {
			success: { type: 'boolean' },
			url: { type: 'string' },
		},
		required: [ 'success', 'url' ],
	},
	callback: async function( input ) {
		const adminUrl = clientAbilitiesDemo.adminUrl || '/wp-admin/';
		let url = adminUrl + input.page;

		// Add query parameters if provided
		if ( input.params ) {
			const params = new URLSearchParams( input.params );
			url += '?' + params.toString();
		}

		// Navigate to the URL
		window.location.href = url;

		return {
			success: true,
			url: url,
		};
	},
} );

unregisterAbility is a convenience method for removing client-side only abilities.

Client-side abilities are saved in the store alongside server-side ones. They are marked specifically with a location property which is used to either call the callback or call the REST endpoint.

The input and output schemas are validated using Ajv and the ajv-formats plugin.

I have configured Ajv to support the intersection of rules that JSON Schema draft-04, WordPress (a subset of JSON Schema draft-04), and providers like OpenAI and Anthropic support.

Ajv is already in use by Gutenberg so it is already part of the ecosystem.

⚠️ Note: Tests and CI setup have been added in #70

Testing

cd packages/client

# Install dependencies
npm install

# Build the package
npm run build

I have created a dummy plugin that registers a few different abilities as an example: client-abilities-demo.zip

Open the developer console and paste the following:

await wp.abilities.listAbilities()

You should see the client side abilities (and any other server side abilities you registered) listed.

Paste in the following:

await wp.abilities.executeAbility('demo/get-current-user')

See that you get a simple user response object back.

Paste in the following:

await wp.abilities.executeAbility('demo/navigate-admin', { 
	page: 'users.php' 
});

See that you are redirected to the users.php screen.

Paste in the following:

wp.abilities.executeAbility('demo/show-notification', { 
	message: 'Hello from client ability!',
	type: 'success'
});

See that a UI notice briefly shows on the user page.

Now we can test registering and unregistering our own ability:

wp.abilities.registerAbility({
    name: 'demo/console-log',
    label: 'Console Logger',
    description: 'Logs messages to the browser console with different levels',
    location: 'client',
    input_schema: {
        type: 'object',
        properties: {
            message: {
                type: 'string',
                description: 'Message to log',
            },
            level: {
                type: 'string',
                enum: ['log', 'info', 'warn', 'error', 'debug'],
                description: 'Console log level',
                default: 'log'
            },
            data: {
                description: 'Additional data to log',
            }
        },
        required: ['message']
    },
    output_schema: {
        type: 'object',
        properties: {
            logged: { type: 'boolean' },
            timestamp: { type: 'string' },
            message: { type: 'string' }
        }
    },
    callback: async ({ message, level = 'log', data }) => {
        const timestamp = new Date().toISOString();
        const prefix = `[WP Ability ${timestamp}]`;

        switch (level) {
            case 'info':
                console.info(prefix, message, data || '');
                break;
            case 'warn':
                console.warn(prefix, message, data || '');
                break;
            case 'error':
                console.error(prefix, message, data || '');
                break;
            case 'debug':
                console.debug(prefix, message, data || '');
                break;
            default:
                console.log(prefix, message, data || '');
        }

        return {
            logged: true,
            timestamp: timestamp,
            message: message
        };
    }
});

Now test the ability:

// Simple log
await wp.abilities.executeAbility('demo/console-log', {
    message: 'Hello from WordPress abilities!'
});

// Warning with data
await wp.abilities.executeAbility('demo/console-log', {
    message: 'Low memory warning',
    level: 'warn',
    data: { available: '100MB', required: '500MB' }
});

// Error log
await wp.abilities.executeAbility('demo/console-log', {
    message: 'Failed to save settings',
    level: 'error',
    data: { code: 'NETWORK_ERROR', retry: true }
});

See that the various responses and different console types are executed.

To test the validation, you can change the return type and see what happens if you try to return a different type or something invalid against the schema.

Finally, you can unregister the client-side ability: wp.abilities.unregisterAbility('demo/console-log');. Trying to execute it again will throw an error.

Next Steps (not addressed in this PR)

Tests & CI setup for the whole client package have been added in #70

@emdashcodes emdashcodes self-assigned this Sep 11, 2025
Copy link

github-actions bot commented Sep 11, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: emdashcodes <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: swissspidy <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Copy link

codecov bot commented Sep 11, 2025

Codecov Report

❌ Patch coverage is 93.06358% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.78%. Comparing base (d774d82) to head (3afbf71).
⚠️ Report is 1 commits behind head on add/client-library.

Files with missing lines Patch % Lines
packages/client/src/api.ts 87.50% 6 Missing ⚠️
packages/client/src/validation.ts 94.31% 5 Missing ⚠️
packages/client/src/store/resolvers.ts 91.66% 1 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                    @@
##             add/client-library      #69      +/-   ##
========================================================
+ Coverage                 77.89%   81.78%   +3.88%     
  Complexity                  103      103              
========================================================
  Files                         9       15       +6     
  Lines                       552      741     +189     
  Branches                      0       75      +75     
========================================================
+ Hits                        430      606     +176     
- Misses                      122      135      +13     
Flag Coverage Δ
javascript 93.12% <93.06%> (?)
unit 77.89% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@swissspidy
Copy link
Member

Do we need permission callbacks on the client as well?

The input and output schemas are validated using Ajv and the ajv-formats plugin.

Probably the best choice.

Curious, how big does this make the bundle?

Ajv is already in use by Gutenberg so it is already part of the ecosystem.

What do mean with "already part of the ecosystem"? It's a mere dev dependency in Gutenberg for use in some tests. It's not in any of the published packages. That's a big difference and not really an argument for picking this dependency.

@emdashcodes emdashcodes force-pushed the add/client-only-abilities branch from ce5aa40 to 082dfdf Compare September 11, 2025 22:21
@emdashcodes
Copy link
Contributor Author

What do mean with "already part of the ecosystem"? It's a mere dev dependency in Gutenberg for use in some tests. It's not in any of the published packages. That's a big difference and not really an argument for picking this dependency.

It's also used in WordPress Playground and a few other utils too. I included it to say we have opted to use it in other places, which to me is worth considering over another package. I haven't looked at the bundle size (yet), but I personally think it is worth it instead of needing to hit a /validate endpoint or something instead for client side abilities.

@emdashcodes
Copy link
Contributor Author

Do we need permission callbacks on the client as well?

I was thinking about this as well and it's worth discussing. I think it would be nice to avoid needing to hit the REST API for running these, but maybe just providing a callback is enough? I'm open to ideas here!

@gziolo
Copy link
Member

gziolo commented Sep 12, 2025

https://bundlephobia.com/package/[email protected]

Screenshot 2025-09-12 at 09 14 48

It's a very reasonable size for the functionality it provides. In the long run we could reuse the logic in Gutenberg to validate block attributes schema in the development mode.


Do we need permission callbacks on the client as well?

It looks like on the server, we will enforce permission callbacks to increase the security as explained by @johnbillion in:

It still might be valuable to have a way to define permission callbacks that checks whether a user can do something in the UI that's only for priviliged users so they have more streamlined experience. Otherwise we risk AI shows too often an message about failed attempe to do something it is not allowed to do. Similarily, @galatanovidiu in this comment #62 (comment) proposed a new property that does more high-level checks whether certain abilities should be filtered out if it's known upfront (without providing specific input) they aren't allowed to use them.

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is shaping up nicely. I left my feedback for consideration and to better understand some decisions made.

@gziolo gziolo changed the title Client-only ability registration Add client-only ability registration Sep 15, 2025
@emdashcodes emdashcodes force-pushed the add/client-only-abilities branch 2 times, most recently from e6510f5 to 398612b Compare September 17, 2025 17:00
@emdashcodes emdashcodes force-pushed the add/client-only-abilities branch from 398612b to 36da8ae Compare September 17, 2025 17:01
@emdashcodes
Copy link
Contributor Author

Regarding permissions, I have added a permissionCallback in f7a3dd2. It will run before an ability is executed. The implementation is intentionally left up to the user. This could be a simple JS based check, or they could hit the API or use nonces. The server permission callback will also still run when the REST API endpoint is called.

@emdashcodes
Copy link
Contributor Author

All PR feedback has been handled correctly now. I'll continue fixing the tests and any other things pointed out in #70.

@emdashcodes emdashcodes requested a review from gziolo September 17, 2025 18:30
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the feedback I shared was addressed. It looks good to land from my side.

* Add testing & linting infa

* Remove uncessary combined checks

* Add client tests

* test: Use proper TypeScript types instead of 'as any' in tests

* Fix typescript check for extra server check

* test: update tests for client-only abilities API changes

* chore: add build:client and dev:client scripts to root package.json

* fix: formatting

* fix: prettier issues

* fix: apply Prettier formatting to README and validation.ts

* deps: remove 		@types/wordpress__api-fetch since types are already shipped

* fix: move validation to the store

* fix: validate ability name uses same format as server

* tests: clean up CI checks

* deps: fix dev dependencies and remove extra package-lock.json

* fix: rename main dev and build commands

* fix: better match server validation rules for schema

* Fix remaining validation issues

Co-authored-by: emdashcodes <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: jonathanbossenger <[email protected]>
@gziolo
Copy link
Member

gziolo commented Sep 19, 2025

I merged #70 into this PR. If all CI checks still pass, I will merge everything together into #60 to review and test all the related changes integrated.

@gziolo gziolo merged commit 3b85b67 into add/client-library Sep 19, 2025
20 checks passed
@gziolo gziolo deleted the add/client-only-abilities branch September 19, 2025 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Type] Enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants