Issue #3586361: Decouple context item form descriptions from scope plugin descriptions.

Description

Adds getItemFormDescription() to AiContextScopeInterface so a scope plugin can show different guidance on a context item form than on the scope overview and settings pages. It defaults to getDescription(), so every existing plugin keeps its current behaviour without an override.

AiContextScopeGlobal already needed this and worked around it privately: it carried a protected getFormDescription() whose docblock read "Uses a line break that is not safe for settings pages escaped with Html::escape(), so keep plain text in getDescription() instead." That was invisible to the API, so no other scope could do the same thing. This promotes it to a documented interface method.

Interface (src/Plugin/AiContextScope/AiContextScopeInterface.php)

  • Added getItemFormDescription().
  • Rewrote the getDescription() docblock. It previously said "Gets the scope description for context items" - the source of the confusion, since the overview and settings pages use it too. It now describes itself as the general description and records that both those pages escape it with Html::escape(), so it must be plain text.
  • Updated the $for_subscription parameter docs on buildValueForm() and added @see cross-references between the three description methods.

Base class - getItemFormDescription() returns getDescription(); buildValueForm() selects it for the item form.

AiContextScopeSiteSection, AiContextScopeTag - same one-line change; both override buildValueForm() and duplicated the ternary.

AiContextScopeTargetEntity - its #description sits in the branch reached only for item forms, so it now uses the new method.

AiContextScopeGlobal - protected getFormDescription() becomes public getItemFormDescription(), warning and <br> unchanged. Removing "Saving the form with this enabled will clear previous scope settings." from the plugin attribute description is no longer part of this MR: #3586359 (closed) / !234 (merged) landed that same change on 1.0.x, so this branch inherits it after the rebase. The method docblock keeps !234 (merged)'s wording and adds why the line break matters.

AiContextScopeOverviewForm - the overview page read getPluginDefinition()['description'] ?? '' directly, bypassing getDescription(). That left the new extension point inoperative on that page: an override was ignored, and a plugin declaring no description got an empty cell rather than the label-derived fallback. It now calls $plugin->getDescription(), which is what scope_api.md and the interface docblock already described. Rendered text is unchanged for every in-tree plugin, confirmed in a browser.

AiContextScopeLanguage needed no change - its single-language branch renders an info item with no #description, and its multi-language branch delegates to parent::buildValueForm().

What the two descriptions look like

The split this MR formalises, on a real site. Both views already render this way on 1.0.x now that !234 (merged) has landed, so these illustrate the mechanism getItemFormDescription() makes available to any scope plugin, rather than a change introduced here.

Scope overview page - the general description, no save/clear warning

image

Context item form - the item form description, warning kept on its own line

image

Documentation - docs/developers/scope_api.md lists all three description methods and gains a Choosing a description method section with a where-it-appears / which-method table, plus a note that getDescription() must be plain text while getItemFormDescription() may contain simple markup. docs/developers/custom_scopes.md no longer describes the attribute description as context-item-specific and links to that section.

Tests

tests/src/Unit/AiContextScopeItemFormDescriptionTest.php (new, 4 tests) covers the default fallback using AiContextScopeUseCase, following the direct-instantiation pattern already used by AiContextScopeAlteredValuesTest: the method equals getDescription(); the fallback also holds for the label-derived description when the plugin definition has no description key; buildValueForm(..., FALSE) prints it; and buildValueForm(..., TRUE) still prints getSubscriptionDescription(). The buildValueForm() test uses a fixture whose general and item form descriptions differ, so it can tell which method the form read - reverting AiContextScopeBase.php:194 fails it.

tests/src/Unit/AiContextScopeGlobalTest.php gains tests for the override. The general-description test builds the plugin from the real #[AiContextScope] attribute by reflection rather than from a fixture, so restoring the warning sentence to the attribute fails it. testBuildValueFormUsesItemFormDescription() was removed as redundant with !234 (merged)'s testBuildValueFormDescription(), which asserts the same #description against the literal expected string and so also catches a wording change.

tests/src/Kernel/AiContextScopePluginTest.php gains one test each for AiContextScopeSiteSection, AiContextScopeTag and AiContextScopeTargetEntity. All three override buildValueForm() and duplicate the description choice, so the base-class test cannot reach them. Each uses a small subclass stub (tests/src/Kernel/*ItemFormDescriptionStub.php) overriding only getItemFormDescription(), leaving the real buildValueForm() to run. Reverting SiteSection:123, Tag:116 or TargetEntity:237 fails the matching test.

Every test above was verified by breaking the production line it guards and confirming the test fails, then restoring it.

Backwards compatibility

Adding a method to AiContextScopeInterface is technically a BC break for a third-party plugin implementing the interface directly instead of extending AiContextScopeBase. Nothing in the module does that - the abstract base is the only direct implementer and supplies a default - so no in-tree plugin needs an override. Noting it because the interface is public API.

Overlap with #3586359 (closed) / !234 (merged)

!234 (merged) has landed on 1.0.x and this branch is rebased on top of it. Two consequences for this diff:

  • Removing the save-warning sentence from the Global plugin attribute is now upstream's change, so that hunk is gone from here. The acceptance criterion is still met, just not by this MR.
  • AiContextScopeGlobalTest::testBuildValueFormUsesItemFormDescription() has been removed. !234 (merged)'s testBuildValueFormDescription() asserts the same #description against the literal expected string, so it catches strictly more than mine did, including a wording change. Keeping both was duplication.

The @defgroup ai_context_scope_api block !234 (merged) added to ai_context.api.php is a signpost to the interface rather than a method reference - it mentions no description method - so the where-it-appears / which-method table stays in docs/developers/scope_api.md alongside the other method tables. Happy to move it if you would rather that guidance lived in api.php.

Testing instructions

Set up per the template, then check out this MR branch and ddev drush cr.

Since !234 (merged) landed, steps 1-3 are regression checks rather than demonstrations. 1.0.x already renders this text the same way: !234 (merged) removed the sentence from the Global attribute and kept the two-line item form description. What this MR adds is the interface method, and the overview page resolving its text through getDescription() - neither is visible without a third-party scope plugin. The cache rebuild is no longer strictly required for plugin definitions, since this MR changes no attribute; run drush cr anyway to clear render caches.

  1. Go to /admin/config/ai/context/settings/scope (Settings > Scope). Every row has a description, and the Global row reads "Always include this context for every agent." without mentioning saving or clearing. Identical to 1.0.x. What changed is that the page now resolves that text through getDescription(), so a plugin overriding the method is honoured and a plugin declaring no description gets the label-derived fallback instead of an empty cell.
  2. Go to /admin/config/ai/context/settings/scope/global. Same text, same absence of the warning.
  3. Go to /admin/config/ai/context/items/add. Expand Context Scope. The Global context checkbox description shows both lines: "Always include this context for every agent." then, after a line break, "Saving the form with this enabled will clear previous scope settings."
  4. On that same form, expand Use Cases, Tags and Site Sections. They read "The use cases / tags / site sections relevant for this context." - unchanged from before this MR. This is the regression check: those plugins do not override the new method, so they fall through to getDescription().
  5. Go to /admin/config/ai/context/settings/agents, edit an agent, and expand Scope Subscriptions. Descriptions still use the subscription wording ("Prioritize context items for these use cases."), and Global is absent - the form's own note confirms "These Scope Plugins Do Not Allow Subscriptions: Global, Target Entities".

Verified locally on Drupal 11.4.4 (standard profile) - all five steps performed in a browser.

Automated verification, all exit 0:

Measured after rebasing onto 1.0.x at 77c4a63. The earlier figures in this description (163 / 519 / 156) predated that rebase, which brought in three upstream commits adding tests of their own.

  • Unit 167 tests
  • Kernel 540 tests
  • Functional 160 tests
  • phpcs clean; PHPStan clean against the module's phpstan.neon.dist
  • cspell reports no new words

Two notes for anyone reproducing this locally:

  • FunctionalJavascript was not run - it needs a Selenium/chromedriver service DDEV does not provide and MINK_DRIVER_ARGS_WEBDRIVER is empty in web/core/phpunit.xml. This change touches no JavaScript.
  • The suite needs the module's whole require-dev installed. Without drupal/diff and drupal/ai_agents_debugger, 24 tests fail across AiContextScopeMapFieldDiffBuilderTest, AiContextItemScopeDiffTest, AiContextItemRevisionUiWithDiffTest, AiContextSchedulerIntegrationTest and AiContextAgentSettingsTest - none of them related to this change.

Checklist

  • I have updated the MR title to use format: Issue #1234: My issue title.
  • I have updated the MR description to include: Closes #1234
  • I have performed a self-review of my own code
  • I have added or updated tests, or explained in the description why this change is not covered by tests
  • I have updated documentation for any new or changed functionality
  • I have written testing instructions and verified them locally
  • I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
  • This MR contains no breaking API or hook changes, or they are explicitly documented in the description

Post-merge step: a cache rebuild is needed, because the #[AiContextScope] attribute's description for the Global scope changed and plugin definitions are cached. No config import or manual change is required.

AI Compliance

Note

Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.

  • AI Assisted Code
    Mainly written by a human; AI used for autocomplete or partial generation under full human supervision.

  • AI Generated Code
    Mainly generated by AI, reviewed and approved by a human before this MR was created.

  • Vibe Coded
    Generated by AI and only functionally reviewed before this MR was created.

Claude Code was used to generate the code changes, both test classes, the documentation edits and this description. Every change was reviewed and applied by hand, the full test suite and static analysis were run locally, and all five testing steps above were performed in a browser before this MR was created.

Closes #3586361 (closed)

Edited by Tekla Aivazashvili

Merge request reports

Loading