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 withHtml::escape(), so it must be plain text. - Updated the
$for_subscriptionparameter docs onbuildValueForm()and added@seecross-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; and "Saving the
form with this enabled will clear previous scope settings." is removed from the
plugin attribute description, so the overview and settings pages no longer
mention saving a context item.
AiContextScopeLanguage needed no change — its single-language branch renders
an info item with no #description, and its multi-language branch delegates to
parent::buildValueForm().
The result
The same plugin, two different descriptions, chosen by which form is rendering:
Scope overview — no save/clear warning
Context item form — warning kept, on its own line
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().
tests/src/Unit/AiContextScopeGlobalTest.php gains 3 tests for the override: the
item form description keeps the warning and the <br>, the general description
does not, and the checkbox prints the item form description. The diff on that
file is insertions only.
Unit suite goes from 156 to 163 tests.
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 / !234
!234 was open while this was written and touches two of the same files. It already
removes the same sentence from the Global plugin attribute (an acceptance
criterion here) and adds a testBuildValueFormDescription() method to
AiContextScopeGlobalTest. This MR branches from 1.0.x; whichever lands first,
the other rebases — the overlap is two small hunks and one test method.
Happy to retarget at 3586359-beta4-polish if you would rather they were stacked,
and happy to move this method's documentation into the
@defgroup ai_context_scope_api block in ai_context.api.php that !234
introduces, rather than docs/developers/scope_api.md, once it lands.
Testing instructions
Set up per the template, then check out this MR branch and ddev drush cr.
The cache rebuild is required, not optional — the change includes the
description: value inside the #[AiContextScope] attribute, and plugin
definitions are cached, so step 1 shows the old text without it.
- Go to
/admin/config/ai/context/settings/scope(Settings → Scope). The Global row's description reads "Always include this context for every agent." and does not mention saving or clearing scope settings. On1.0.xit currently also says "Saving the form with this enabled will clear previous scope settings.", which is meaningless on an overview page. - Go to
/admin/config/ai/context/settings/scope/global. Same text, same absence of the warning. - 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." - 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(). - 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:
- Unit 163 tests / 365 assertions (baseline 156 / 353 on a clean checkout of the same commit before any edit)
- Kernel 519 tests / 8,425 assertions
- Functional 156 tests / 2,073 assertions
- phpcs clean; PHPStan clean against the module's
phpstan.neon.dist - cspell reports no new words
Two notes for anyone reproducing this locally:
FunctionalJavascriptwas not run — it needs a Selenium/chromedriver service DDEV does not provide andMINK_DRIVER_ARGS_WEBDRIVERis empty inweb/core/phpunit.xml. This change touches no JavaScript.- The suite needs the module's whole
require-devinstalled. Withoutdrupal/diffanddrupal/ai_agents_debugger, 24 tests fail acrossAiContextScopeMapFieldDiffBuilderTest,AiContextItemScopeDiffTest,AiContextItemRevisionUiWithDiffTest,AiContextSchedulerIntegrationTestandAiContextAgentSettingsTest— 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

