Abilities Explorer: move to DataViews and REST - #1074
Conversation
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]>
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]>
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
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 (
#1004's e2e job passes in CI, so these look like local environment failures. CI on this PR is the authoritative full run. |
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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 was the work here needed in the AI Workspace PR branch? This PR probably could've been off |
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/dataviewsscreen backed by four newai/v1routes. Stacked onfeat/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:ai/v1/abilitiesshow_in_rest, plus the assistant policy stateai/v1/abilities/item?name=ai/v1/abilities/invokeWP_Ability::execute(), so its own permission check still appliesai/v1/abilities/surfaceAll four share one permission check:
manage_options, cookie authentication, a validwp_restnonce, 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 existingactionandabilityquery 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-hookscan add read-only fields with theai.abilitiesExplorer.fieldsfilter. 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 indocs/experiments/abilities-explorer.md.Behavior changes worth knowing:
is_admin()is false. The route loadswp-admin/includes/admin.phpfirst.is_admin()is true no longer appears.trace.ai_ability_explorer_invokeand theAbility_Tableclass are removed.s,orderbyor filter args land on the unfiltered list.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
ai/content-classification,max_suggestions11 should report "must be at most 10", and 5 should validate.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.tests/e2e/specs/experiments/abilities-explorer-list.spec.js:tests/e2e/specs/experiments/abilities-explorer-surface.spec.js:tests/e2e/specs/experiments/abilities-explorer-runner.spec.js:tests/e2e/specs/experiments/abilities-explorer-navigation.spec.js: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_enabledtest_rest_routes_are_absent_when_disabledAbility_HandlerTest(5):test_validate_input_accepts_a_type_listtest_generate_example_input_returns_empty_for_empty_schematest_generate_example_input_uses_default_valuestest_generate_example_input_uses_example_valuestest_generate_example_input_generates_type_defaultsAdmin_PageTest(12):test_load_hook_registers_help_tabs_and_assetstest_assets_are_hooked_only_when_the_screen_loadstest_enqueue_assets_enqueues_bundle_and_dataviews_fallbacktest_enqueue_assets_skips_dataviews_fallback_when_core_registers_ittest_enqueue_assets_does_nothing_for_an_editortest_localized_settings_route_map_matches_controller_constantstest_localized_settings_carry_reason_and_provider_labelstest_render_page_works_for_admintest_render_page_ignores_action_for_server_rendertest_render_page_outputs_nothing_for_editortest_render_page_outputs_nothing_when_logged_outtest_help_tabs_register_on_screenAbilities_ControllerTest(48):test_list_includes_an_ability_not_shown_in_resttest_list_keeps_origin_and_provider_apart_and_sends_the_category_label_unescapedtest_list_decodes_a_category_label_escaped_by_a_filtertest_list_survives_an_ability_with_unencodable_metatest_item_returns_schemas_and_example_inputtest_item_returns_404_for_an_unregistered_nametest_item_returns_400_for_a_missing_nametest_invoke_with_valid_input_returns_the_datatest_invoke_with_input_failing_explorer_validation_returns_400test_invoke_with_input_failing_core_validation_returns_the_ability_errortest_invoke_accepts_a_property_typed_with_a_type_listtest_invoke_without_an_input_schema_accepts_empty_and_nulltest_invoke_with_input_omitted_invokes_with_no_inputtest_invoke_decodes_scalar_input_on_the_servertest_invoke_with_malformed_json_returns_400test_invoke_refuses_non_string_inputtest_invoke_returns_404_for_an_unregistered_abilitytest_invoke_reports_the_ability_permission_denial_as_its_outcometest_invoke_writes_no_request_log_rowtest_surface_remove_stores_the_exclusion_and_a_repeat_is_no_changetest_surface_restore_clears_the_exclusiontest_surface_restore_of_a_withheld_ability_keeps_it_off_the_assistanttest_surface_policy_switch_disables_and_enables_and_returns_the_listtest_surface_refuses_an_unknown_changetest_surface_remove_refuses_an_unregistered_or_missing_nametest_surface_get_changes_nothingtest_list_reports_only_known_originstest_list_carries_a_custom_provider_beside_the_known_originstest_list_lets_a_custom_provider_row_match_its_origin_and_its_labeltest_list_marks_a_declared_admitted_ability_as_on_the_assistanttest_list_carries_the_reason_an_ability_is_off_the_assistanttest_list_and_function_declaration_share_one_description_sourcetest_list_does_not_report_a_removed_ability_as_held_without_the_workspace_bootstraptest_list_reports_the_general_public_flag_on_both_channelstest_list_reports_rest_and_mcp_exposure_independentlytest_surface_remove_takes_the_ability_off_the_model_declarationstest_surface_restore_returns_the_ability_to_the_model_declarationstest_surface_policy_switch_withdraws_and_returns_admitted_abilitiestest_editor_is_refused_on_every_routetest_application_password_is_refused_on_every_routetest_application_password_is_refused_even_alongside_the_cookie_flagtest_determine_current_user_without_a_cookie_is_refused_on_every_routetest_cookie_administrator_without_a_nonce_is_refused_on_every_routetest_cookie_administrator_with_an_invalid_nonce_is_refused_on_every_routetest_cookie_administrator_with_a_nonce_for_another_action_is_refused_on_every_routetest_cookie_administrator_with_the_nonce_as_a_parameter_is_acceptedtest_cookie_administrator_reaches_every_routetest_no_route_is_registered_as_an_abilityUnapplied review findings
From the branch's code review, not blocking:
src/experiments/abilities-explorer/api.ts— The response sequencer andvalidate.tshave no JS unit tests, because the repo has no JS unit test setup. They are covered only through e2e.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.src/experiments/abilities-explorer/fields.tsx—BUILT_IN_FIELD_IDSis a hand-kept list. Deriving it fromgetBuiltInFields()would stop a new built-in field from being overridable.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
🤖 Generated with Claude Code