Skip to content

Abilities Explorer: move to DataViews and REST - #1074

Merged
whyisjake merged 16 commits into
feat/ai-workspacefrom
feat/abilities-explorer-dataviews
Sep 29, 2026
Merged

whyisjake merged 16 commits into
feat/ai-workspacefrom
feat/abilities-explorer-dataviews

Conversation

@whyisjake

@whyisjake whyisjake commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

What?

Closes #1016. See #203.

Moves the whole Abilities Explorer screen (list, statistics, the "Exposed in" column and its actions, detail view, test runner) off WP_List_Table, vanilla JS and admin-ajax onto a @wordpress/dataviews screen backed by four new ai/v1 routes. Stacked on feat/ai-workspace (#1004), because the "Exposed in" column and its actions exist only there.

Why?

The Explorer was the plugin's last admin screen on the old stack, and its hand-written search, sort and filters kept needing repair (#883, #641, #648, #344, #588). The "Exposed in" column answers the question an owner most needs answered, which abilities the assistant can reach, but it could not be filtered without more hand-written filtering. #203 asked for a custom-column hook, which would have shipped an API against a table about to be replaced.

How?

REST routes (includes/Experiments/Abilities_Explorer/REST/Abilities_Controller.php), registered only while the experiment is on and never as abilities:

Route Method Purpose
ai/v1/abilities GET every registered ability, including ones without show_in_rest, plus the assistant policy state
ai/v1/abilities/item?name= GET one ability with schemas, raw data and example input
ai/v1/abilities/invoke POST runs the ability through WP_Ability::execute(), so its own permission check still applies
ai/v1/abilities/surface POST remove, restore, disable_policy or enable_policy

All four share one permission check: manage_options, cookie authentication, a valid wp_rest nonce, and no application password. The nonce is checked in the callback itself because core skips its own check when an earlier authentication filter has already answered. Ability names travel in the query string or body, never the path, so an encoded / cannot 404 on Apache. Each list item is encode-checked on its own, so one ability with unencodable data cannot blank the list.

Screen (src/experiments/abilities-explorer/): a React app mounted like the AI Request Logs page. It routes between list, detail, runner and not-found on the existing action and ability query args, with browser Back and Forward. The provider filter keeps the #883 rule (origin for Core, Plugin and Theme, exact label otherwise). "Exposed in" is now filterable by assistant state. Surface changes wait for the server, and responses carry a sequence number, tracked per row, so a late refresh cannot undo a newer change.

#203: plugins that depend on wp-hooks can add read-only fields with the ai.abilitiesExplorer.fields filter. Built-in field IDs win on a collision, a filter that throws or returns a non-array leaves the built-ins, and a saved view keeps a third-party column's ID while its plugin is inactive. Documented in docs/experiments/abilities-explorer.md.

Behavior changes worth knowing:

  • Invoke now runs under REST, where is_admin() is false. The route loads wp-admin/includes/admin.php first.
  • The list is built from a REST request too, so an ability a plugin registers only when is_admin() is true no longer appears.
  • Errors show the ability's code, message and data instead of the always-null trace.
  • The admin-ajax action ai_ability_explorer_invoke and the Ability_Table class are removed.
  • Old list bookmarks with s, orderby or filter args land on the unfiltered list.
  • Back to List keeps the view's layout, columns, sort and page size. Search and filters reset.
  • Row actions (View, Test, and Remove from or Return to assistant) sit under each ability's name and show on hover or keyboard focus, as in WP_List_Table. They are always shown below 782px.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Planning, implementation of the REST controller, React screen, tests and docs, and a multi-reviewer code review whose confirmed findings were fixed in this branch.

Testing Instructions

  1. Enable Experiments and the Abilities Explorer experiment, then open Tools > Abilities Explorer.
  2. Filter by Provider, Category and "Exposed in", search, sort and page. The statistics should not change with search or filters.
  3. With the AI Workspace experiment on, remove an ability from the assistant, return it, and turn the admission policy off and on. Each change should show a notice.
  4. Open an ability's detail view, copy a schema, then open Test Ability. For ai/content-classification, max_suggestions 11 should report "must be at most 10", and 5 should validate.
  5. Use browser Back and reload on each view.

Tests run locally

  • npm run build, npm run typecheck, npm run lint:js, npm run lint:php, npm run lint:php:stan: all pass.
  • npm run test:php: 1988 tests, 40 skipped, 0 failures.
  • Explorer e2e, 24 tests across four files, all passing:
    • tests/e2e/specs/experiments/abilities-explorer-list.spec.js:
      • shows the statistics and the table under a single heading
      • narrows the rows with the "Category" filter
      • matches "Plugin" by origin and "Acme" by its exact label
      • keeps the statistics unaffected by search
      • filters "Exposed in" down to what the assistant can reach
      • restores layout, fields and sort from a saved view, but not search or filters
      • loads a saved view with an unknown field ID and keeps the ID
    • tests/e2e/specs/experiments/abilities-explorer-surface.spec.js:
      • removes an ability and returns it, with a notice for each
      • returning a withheld ability shows the withheld reason, not the assistant badge
      • turns the admission policy off and on, updating every row
      • disables a pending change, so a second click sends nothing
      • does not let a refresh answered after a remove undo it
      • moves focus to the list when a change filters its row out
    • tests/e2e/specs/experiments/abilities-explorer-runner.spec.js:
      • shows each detail section and copies the JSON
      • validates deep-linked input against the schema
      • shows the success and error panels, and Clear hides them
      • never shows one runner the result of another ability in flight
    • tests/e2e/specs/experiments/abilities-explorer-navigation.spec.js:
      • keeps one heading per view, and Back and reload keep the view
      • fetches an ability again when it is opened after returning to the list
      • shows an extension column on first load and keeps it through deactivation
      • keeps the built-in columns when the filter returns a non-array
      • does not let an extension replace a built-in field
      • is unreachable when the experiment is off
      • is unreachable when AI is globally off
  • Full npm run test:e2e: running at the time this PR was opened; the result will be added below.
PHPUnit tests added or moved in this branch (67)

Abilities_ExplorerTest (2):

  • test_rest_routes_are_registered_when_enabled
  • test_rest_routes_are_absent_when_disabled

Ability_HandlerTest (5):

  • test_validate_input_accepts_a_type_list
  • test_generate_example_input_returns_empty_for_empty_schema
  • test_generate_example_input_uses_default_values
  • test_generate_example_input_uses_example_values
  • test_generate_example_input_generates_type_defaults

Admin_PageTest (12):

  • test_load_hook_registers_help_tabs_and_assets
  • test_assets_are_hooked_only_when_the_screen_loads
  • test_enqueue_assets_enqueues_bundle_and_dataviews_fallback
  • test_enqueue_assets_skips_dataviews_fallback_when_core_registers_it
  • test_enqueue_assets_does_nothing_for_an_editor
  • test_localized_settings_route_map_matches_controller_constants
  • test_localized_settings_carry_reason_and_provider_labels
  • test_render_page_works_for_admin
  • test_render_page_ignores_action_for_server_render
  • test_render_page_outputs_nothing_for_editor
  • test_render_page_outputs_nothing_when_logged_out
  • test_help_tabs_register_on_screen

Abilities_ControllerTest (48):

  • test_list_includes_an_ability_not_shown_in_rest
  • test_list_keeps_origin_and_provider_apart_and_sends_the_category_label_unescaped
  • test_list_decodes_a_category_label_escaped_by_a_filter
  • test_list_survives_an_ability_with_unencodable_meta
  • test_item_returns_schemas_and_example_input
  • test_item_returns_404_for_an_unregistered_name
  • test_item_returns_400_for_a_missing_name
  • test_invoke_with_valid_input_returns_the_data
  • test_invoke_with_input_failing_explorer_validation_returns_400
  • test_invoke_with_input_failing_core_validation_returns_the_ability_error
  • test_invoke_accepts_a_property_typed_with_a_type_list
  • test_invoke_without_an_input_schema_accepts_empty_and_null
  • test_invoke_with_input_omitted_invokes_with_no_input
  • test_invoke_decodes_scalar_input_on_the_server
  • test_invoke_with_malformed_json_returns_400
  • test_invoke_refuses_non_string_input
  • test_invoke_returns_404_for_an_unregistered_ability
  • test_invoke_reports_the_ability_permission_denial_as_its_outcome
  • test_invoke_writes_no_request_log_row
  • test_surface_remove_stores_the_exclusion_and_a_repeat_is_no_change
  • test_surface_restore_clears_the_exclusion
  • test_surface_restore_of_a_withheld_ability_keeps_it_off_the_assistant
  • test_surface_policy_switch_disables_and_enables_and_returns_the_list
  • test_surface_refuses_an_unknown_change
  • test_surface_remove_refuses_an_unregistered_or_missing_name
  • test_surface_get_changes_nothing
  • test_list_reports_only_known_origins
  • test_list_carries_a_custom_provider_beside_the_known_origins
  • test_list_lets_a_custom_provider_row_match_its_origin_and_its_label
  • test_list_marks_a_declared_admitted_ability_as_on_the_assistant
  • test_list_carries_the_reason_an_ability_is_off_the_assistant
  • test_list_and_function_declaration_share_one_description_source
  • test_list_does_not_report_a_removed_ability_as_held_without_the_workspace_bootstrap
  • test_list_reports_the_general_public_flag_on_both_channels
  • test_list_reports_rest_and_mcp_exposure_independently
  • test_surface_remove_takes_the_ability_off_the_model_declarations
  • test_surface_restore_returns_the_ability_to_the_model_declarations
  • test_surface_policy_switch_withdraws_and_returns_admitted_abilities
  • test_editor_is_refused_on_every_route
  • test_application_password_is_refused_on_every_route
  • test_application_password_is_refused_even_alongside_the_cookie_flag
  • test_determine_current_user_without_a_cookie_is_refused_on_every_route
  • test_cookie_administrator_without_a_nonce_is_refused_on_every_route
  • test_cookie_administrator_with_an_invalid_nonce_is_refused_on_every_route
  • test_cookie_administrator_with_a_nonce_for_another_action_is_refused_on_every_route
  • test_cookie_administrator_with_the_nonce_as_a_parameter_is_accepted
  • test_cookie_administrator_reaches_every_route
  • test_no_route_is_registered_as_an_ability

Unapplied review findings

From the branch's code review, not blocking:

  • P3 — src/experiments/abilities-explorer/api.ts — The response sequencer and validate.ts have no JS unit tests, because the repo has no JS unit test setup. They are covered only through e2e.
  • P3 — includes/Experiments/Abilities_Explorer/REST/Abilities_Controller.php — The invoke success payload is not passed through the strict encode check the list and item use, so an ability returning NaN or INF produces a generic client error.
  • P3 — src/experiments/abilities-explorer/fields.tsx — BUILT_IN_FIELD_IDS is a hand-kept list. Deriving it from getBuiltInFields() would stop a new built-in field from being overridable.
  • P3 — Response ordering uses one server's microtime(). On multi-node hosting with clock skew, a stale list could briefly show a row's old state until the next refresh.

Changelog Entry

Changed - Rebuilt the Abilities Explorer on DataViews with REST routes, a filterable "Exposed in" column, and a JavaScript filter for adding read-only table fields.

🤖 Generated with Claude Code

Open WordPress Playground Preview

whyisjake and others added 13 commits September 29, 2026 10:56
List, item, invoke and surface routes for the upcoming DataViews screen.
All four share one permission check: manage_options, cookie
authentication (so core's REST nonce check applied), and no
application password. Ability names travel in the query string or
body. Each list item is encode-checked on its own so one bad
registrant cannot blank the list. Example input generation moves
into Ability_Handler.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page now renders a single root that the new bundle mounts on,
enqueued only on the Explorer screen, with the DataViews stylesheet
fallback and a route map built from the REST controller's constants.
The app routes between list, detail, test runner and not-found views
on the existing action and ability query arguments, renders one h1,
shows snackbars, and replaces its controls with one explanation when
the experiment is turned off or access is lost. The list, detail and
runner views are placeholders for now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Name, slug, provider and "Exposed in" columns with search, sorting,
20 rows per page, and Category, Provider and "Exposed in" filters.
The provider filter keeps the #883 rule (origin for Core, Plugin and
Theme, exact label otherwise) by handling that filter before
DataViews does. Statistics count every ability by origin regardless
of search or filters. Saved views keep only layout, fields, sort and
page size, and never drop a field ID they do not recognise. The e2e
fixture plugin registers an ability with a custom "Acme" provider.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The detail view shows description, provider, both schemas and raw
data, each JSON block with Unicode kept and a Copy button. The test
runner prefills example input once per ability, validates against
the schema without crashing on non-object input, sends the raw JSON
string to the invoke route, and ignores a late response for another
ability. Errors now show the ability's code, message and data.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
One global sequence number dropped the older of two row responses
that answered out of order, even though they covered different rows.
Track sequence numbers per row, for the full list and for the policy
state instead, so a row response only competes with responses that
cover that row, and a full list read before a row change keeps the
row's newer state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Answers #203. Code that depends on wp-hooks can add fields through
the ai.abilitiesExplorer.fields filter. Built-in field IDs win on a
collision, a filter that throws or returns a non-array leaves the
built-in fields, and extension fields are read-only with no actions.
Fields registered after the list renders still appear. Saved views
keep a third-party field's ID while its plugin is inactive, so its
column comes back on reactivation. An e2e fixture plugin adds one
field for the end-to-end tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rows on the assistant offer "Remove from assistant" and owner-excluded
rows offer "Return to assistant". A policy toggle above the table
switches the admission policy. Changes wait for the server, keep the
control busy and ignore a second click, and confirm themselves with
today's wording in a visible, announced snackbar. Focus stays on the
control, or moves to the table when the row leaves an active filter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… stack

Deletes Ability_Table, the vanilla JS client and the two admin-ajax
handlers now that the React screen covers every view. The table's
tests move onto the REST payload in the controller test. The docs
describe the React screen, its four routes, and the behavior changes:
invoke runs under REST, errors show a code and data instead of trace,
and Back to List keeps the view's layout. The e2e spec is rewritten
around role-based locators and covers the list, surface actions,
detail view, test runner, navigation, field extensions and
availability, starting each test from a known state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Shares one provider badge and one record guard, drops TestRunner
state that its per-ability remount already made redundant, keeps a
single reason-label map in Ability_Handler, resolves ability names in
one place in the controller, memoizes JSON formatting, skips saved
view writes that would not change the stored record, and rewrites
comments that pointed at removed code or planning notes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Input validation passed a JSON Schema type list such as
["string", "null"] to a string parameter, so invoking such an ability
through the REST route failed with a TypeError. A value is now valid
when it matches any listed type, as the client validator already
allowed.

The shared permission check now verifies the wp_rest nonce itself.
Core skips its own nonce check when an earlier authentication filter
has already answered, so relying on it made the guard depend on the
order other plugins' filters run in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The held item was reused whenever the same ability was opened again,
so a failed or missing load, or data older than the list the user
just refreshed, came back without a request. Returning to the list
now drops the held item; moving between the detail view and the
runner for the same ability still reuses it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Explorer spec grew past a thousand lines, so its shared helpers
move to tests/e2e/utils and its groups become four spec files with
unchanged test names. The in-flight runner test now waits for the held
invoke to settle instead of sleeping, and a new navigation test checks
that reopening an ability from the list fetches it again. The docs
note that the screen reads abilities from REST requests and that its
routes require an administrator's browser session and REST nonce.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@whyisjake
whyisjake marked this pull request as ready for review September 29, 2026 20:18
@whyisjake
whyisjake requested review from a team and jeffpaul as code owners September 29, 2026 20:18
@github-actions

Copy link
Copy Markdown

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: whyisjake <[email protected]>

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

@whyisjake

whyisjake commented Sep 29, 2026 •

Copy link
Copy Markdown
Member Author

Correction: my earlier note said the local full e2e run stopped partway. It did not; I read its log before it had finished. The complete local run (npm run test:e2e, chromium) against this branch finished with:

  • 252 passed, including every Abilities Explorer test in the four new abilities-explorer-*.spec.js files.
  • 10 failed, none in the Explorer specs:
    • Seven fail the same way locally without this branch's changes:

      • alt-text-generation.spec.js:306
      • bulk-content-summarization.spec.js:113
      • content-classification.spec.js:430
      • editorial-notes.spec.js:307
      • slug-generation.spec.js:238 and :300
      • image-editing.spec.js:57

      I re-ran editorial-notes.spec.js:307, both slug-generation tests and image-editing.spec.js:57 on the base commit (fa27c32), and they fail identically. image-editing.spec.js:57 expects two preview images and gets one, on both commits.

    • Three passed when re-run in isolation: title-generation.spec.js:31 and :167, and image-generation.spec.js:249. They had run while the same local site was being changed by hand.

#1004's e2e job passes in CI, so these look like local environment failures. CI on this PR is the authoritative full run.

@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.30303% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.20%. Comparing base (fa27c32) to head (88aa03b).

Files with missing lines Patch % Lines
...Experiments/Abilities_Explorer/Ability_Handler.php 53.84% 30 Missing ⚠️
...s/Abilities_Explorer/REST/Abilities_Controller.php 99.11% 2 Missing ⚠️
Additional details and impacted files
@@                   Coverage Diff                   @@
##             feat/ai-workspace    #1074      +/-   ##
=======================================================
+ Coverage                81.79%   83.20%   +1.41%     
+ Complexity                3874     3836      -38     
=======================================================
  Files                      150      150              
  Lines                    14735    14531     -204     
=======================================================
+ Hits                     12052    12091      +39     
+ Misses                    2683     2440     -243     
Flag Coverage Δ
unit 83.20% <90.30%> (+1.41%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

whyisjake and others added 3 commits September 29, 2026 13:35
View, Test and the assistant surface action now sit under each
ability's name, as in WP_List_Table, instead of in a separate sticky
Actions column. That column took the table's spare width and let the
"Exposed in" content run underneath it. The actions stay visible
rather than appearing on hover, and "Exposed in" gets a minimum width
that older saved views pick up too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…Table

Row actions now stay off-screen until the row is hovered or holds
keyboard focus, and are always shown below 782px, matching core's
.row-actions. They remain in the page for screen readers and keyboard
users, and rows keep their height. The e2e specs hover a row before
using its actions, as a mouse user would.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
When a surface change takes the only matching row out of an active
"Exposed in" filter, DataViews renders no table, so focus had nowhere
reliable to land and the e2e test failed in CI. Focus now always moves
to the list's container, a region named "Abilities list", which exists
whether or not any rows remain.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@whyisjake
whyisjake merged commit 0c96aab into feat/ai-workspace Sep 29, 2026
33 of 34 checks passed
@whyisjake
whyisjake deleted the feat/abilities-explorer-dataviews branch September 29, 2026 23:11
@jeffpaul

Copy link
Copy Markdown
Member

@whyisjake was the work here needed in the AI Workspace PR branch? This PR probably could've been off develop directly to consider this on its own for the plugin?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants