Merge pull request #5210 from deepseek-harness/fix/plugin-inventory-card-align

fix(ui-settings-plugin-inventory): align and simplify plugin cards
This commit is contained in:
Ziya authored and GitHub committed 2026-09-26 06:44:15 -07:00
commit d70db1ccfd
10 files changed
+237 -132

No files matched your search

@@ -1,23 +1,20 @@
- list:
- listitem:
- button "tool-subagent-control, tool-subagent-control, 已启用":
- button "tool-subagent-control, 已启用":
- strong: tool-subagent-control
- text: 已启用 Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations
- code: tool-subagent-control
- text: Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations
- listitem:
- button "tool-subagent-control/list-agents, tool-subagent-list-agents, 已启用":
- strong: tool-subagent-control/list-agents
- text: 已启用
- code: tool-subagent-list-agents
- listitem:
- button "tool-subagent, tool-subagent, 已启用":
- button "tool-subagent, 已启用":
- strong: tool-subagent
- text: 已启用 Model-facing subagent delegation tool over the ctx.subagents seam
- code: tool-subagent
- text: Model-facing subagent delegation tool over the ctx.subagents seam
- listitem:
- button "tool-subagent, tool-subagent-fork, 已启用":
- strong: tool-subagent
- text: 已启用 Model-facing subagent delegation tool over the ctx.subagents seam
- text: Model-facing subagent delegation tool over the ctx.subagents seam
- code: tool-subagent-fork
- listitem:
- button "tool-subagent, tool-subagent-codex, 已停用":
@@ -1,5 +1,4 @@
- listitem:
- button "tool-subagent, tool-subagent, 已启用":
- button "tool-subagent, 已启用":
- strong: tool-subagent
- text: 已启用 Model-facing subagent delegation tool over the ctx.subagents seam
- code: tool-subagent
- text: Model-facing subagent delegation tool over the ctx.subagents seam
+20 -11
View File
@@ -111,16 +111,17 @@ describe('web e2e: settings modal and General preferences', () => {
// rewrite this surface's golden.
await dialog.getByRole('button', { name: '内置插件', exact: true }).click()
await dialog.getByRole('heading', { name: '内置插件', exact: true }).waitFor({ timeout: 10_000 })
// Both groups start collapsed; the preset group's header still carries its display-only switcher.
// The preset group starts open under its display-only switcher; the global group starts folded.
const presetSwitcher = dialog.getByRole('button', { name: '选择要查看的 Agent 预设' })
await presetSwitcher.waitFor({ timeout: 10_000 })
// The shipped default's zh display name comes from the zh dictionaries.
expect(await presetSwitcher.textContent()).toBe('标准模式(默认)')
const presetToggle = dialog.getByRole('button', { name: '会话插件', exact: true })
expect(await presetToggle.getAttribute('aria-expanded')).toBe('false')
expect(await dialog.locator('[data-plugin-scope="preset"] [data-plugin-entry]').count()).toBe(0)
await presetToggle.click()
await dialog.getByRole('button', { name: /^全局/ }).click()
expect(await presetToggle.getAttribute('aria-expanded')).toBe('true')
expect(await dialog.locator('[data-plugin-scope="preset"] [data-plugin-entry]').count()).toBeGreaterThan(0)
const globalToggle = dialog.getByRole('button', { name: /^全局/ })
expect(await globalToggle.getAttribute('aria-expanded')).toBe('false')
await globalToggle.click()
const pluginRow = dialog.locator(PLUGIN_ROW_SELECTOR)
await pluginRow.waitFor({ timeout: 10_000 })
const expectedPluginCount = [...scaffold.ctx.loader.entries()]
@@ -132,11 +133,12 @@ describe('web e2e: settings modal and General preferences', () => {
// presets took over included, preset compositions excluded.
expect(await dialog.locator('[data-plugin-scope="global"] [data-plugin-entry]').count())
.toBe(expectedPluginCount)
// The enablement tag is the row's collapsed status: an active fiber draws no
// dot, so no global row names the active phase. Guard the assertion against
// matching nothing because no row is enabled.
expect(await dialog.locator('[data-plugin-scope="global"] [data-plugin-entry] button[aria-label$="已启用"]').count())
.toBeGreaterThan(0)
// A plainly enabled row states its status only in its accessible name: it
// carries no tag, and an active fiber draws no dot, so no global row shows
// either. Guard the assertions against matching nothing because no row is enabled.
const enabledRows = dialog.locator('[data-plugin-scope="global"] [data-plugin-entry] button[aria-label$="已启用"]')
expect(await enabledRows.count()).toBeGreaterThan(0)
expect(await enabledRows.getByText('已启用', { exact: true }).count()).toBe(0)
expect(await dialog.locator('[data-plugin-scope="global"] [role="img"][aria-label="运行中"]').count()).toBe(0)
expect(await dialog.locator('[data-plugin-count]').getAttribute('data-plugin-count'))
.toBe(String(expectedPluginCount))
@@ -159,10 +161,17 @@ describe('web e2e: settings modal and General preferences', () => {
] as const
for (const [entryId, status] of instanceRows) {
const row = dialog.locator(`[data-plugin-scope="preset"] [data-plugin-entry="${entryId}"]`)
const trigger = row.getByRole('button', { name: `tool-subagent, ${entryId}, ${status}`, exact: true })
// An id that repeats the title adds nothing, so its card shows no chip and its name omits it.
const repeatsTitle = entryId === 'tool-subagent'
const name = repeatsTitle ? `tool-subagent, ${status}` : `tool-subagent, ${entryId}, ${status}`
const trigger = row.getByRole('button', { name, exact: true })
await trigger.waitFor({ timeout: 10_000 })
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
const identity = row.locator('code')
if (repeatsTitle) {
expect(await identity.count()).toBe(0)
continue
}
expect(await identity.textContent()).toBe(entryId)
expect(await identity.getAttribute('title')).toBe(entryId)
}
@@ -9,8 +9,8 @@
en: 463eecf002ec0f54
zh: b8dc6003ddccd14a
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/summary:
en: 56f5349b6e502525
zh: 691a0fe90b739e9d
en: f52353cf99816eb8
zh: e172c1751289882f
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -18,14 +18,14 @@
en: 67637808e7940157
zh: d4d0716ea4d33e4a
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/use-this-package/reading-a-card:
en: e615aaa891b5055f
zh: 38769e763c8e54ca
en: 141d6a73e99fee51
zh: 2aa28b174867c967
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/use-this-package/the-preset-switcher:
en: 84e5938329199a22
zh: 6f5cfaac56433e8b
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/use-this-package/retrying-a-failed-read:
en: 2859449aaae5ab59
zh: 06380059ae429026
en: 673c7574e14078be
zh: b093e897f0341973
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/understand-the-implementation:
en: cab883d26642c932
zh: 54e7321bd781b400
@@ -33,8 +33,8 @@
en: e4792880c5916ce0
zh: 4c8171a446dd153f
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/understand-the-implementation/rendering:
en: b9aa850e5f101b68
zh: 8a29025f19f6ce37
en: 779146e3d3732803
zh: 0dfafa328dfeda75
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/further-exploration:
en: ae3819f0c5da95bd
zh: 40972ab96d5c95e2
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It lists agent presets, then the global inventory; both start collapsed and open on demand or during a search. Cards show localized titles and descriptions when available, identify instances by stable entry id, and expose enablement, source details, runtime status, disabled conditions, and discovery failures; preset-provided global entries name their presets. Search covers both groups and points to matches in other presets. The tab handles loading, empty, no-match, failure, and retry states without exposing transport details, and still shows the global inventory without a preset roster.
The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It lists agent presets, open by default, then the collapsed global inventory; a search opens both. Cards show localized titles and descriptions when available, identify instances by stable entry id, and expose enablement, source details, runtime status, disabled conditions, and discovery failures; preset-provided global entries name their presets. Search covers both groups and points to matches in other presets. The tab handles loading, empty, no-match, failure, and retry states without exposing transport details, and still shows the global inventory without a preset roster.
## Table of Contents
@@ -31,9 +31,9 @@ A failed installation with pending pnpm build permissions offers **Allow these s
### Reading a card
Each collapsed card uses the plugin's localized title and description when available. Settings shortens literal package-name or module-name fallbacks by removing the npm scope and the `cordis:`, `cordis-plugin-`, or `dsh-`/`dsh-host-`/`dsh-client-` prefix; locale titles remain unchanged. The Host supplies locale and package-field fallbacks in `meta`; the Client selects the current language. Full module specifiers remain available in details and search. Missing descriptions are omitted, and metadata diagnostics appear with the card. This compact naming applies only to Settings, not the sidebar's Plugins page.
Each collapsed card uses the plugin's localized title and description when available; a description shows at most two lines until its card is expanded. Settings shortens literal package-name or module-name fallbacks by removing the npm scope and the `cordis:`, `cordis-plugin-`, or `dsh-`/`dsh-host-`/`dsh-client-` prefix; locale titles remain unchanged. The Host supplies locale and package-field fallbacks in `meta`; the Client selects the current language. Full module specifiers remain available in details and search. Missing descriptions are omitted, and metadata diagnostics appear with the card. This compact naming applies only to Settings, not the sidebar's Plugins page.
The stable entry id appears underneath the title, alongside a small enablement tag; a colored root-fiber status dot marks only the phases the tag cannot state: `pending` maps to idle, while `loading` and `unloading` map to ongoing. An `active` or `failed` fiber shows its tag without a dot. A composition-generated subtitle omits its leading `include:` marker, while hover, search, the accessible name, and expanded details retain the complete id. Long entry ids truncate in the row and remain available on hover. Expanding one card reveals the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-preset-registry/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset declarations' Chinese names. Search filters both groups by localized title and description, module name, and entry id.
A small enablement tag marks only the states that differ from plainly enabled: disabled, conditional, provided by presets, and failed; a plainly enabled row carries no tag. A colored root-fiber status dot marks the live phases no tag states: `pending` maps to idle, while `loading` and `unloading` map to ongoing; an `active` or `failed` fiber shows no dot. The stable entry id appears in a code chip at the card's bottom-left, in line with the title, only when it differs from the title: a composition-generated id drops its leading `include:` marker there, and an id that then repeats the title is omitted. Whenever the chip shows, its hover text and the card's accessible name carry the complete id; search and expanded details always retain it. Long entry ids truncate in the chip and remain available on hover. Expanding one card reveals its full description, the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-preset-registry/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset declarations' Chinese names. Search filters both groups by localized title and description, module name, and entry id.
### The preset switcher
@@ -41,7 +41,7 @@ The switcher is the same selector-pill-plus-menu control the General settings ro
### Retrying a failed read
A failed read renders the shared error marker inside the tab; loading and current-page synchronization use the shared ongoing loader. Retrying re-runs the lazy `list()` call without exposing transport details.
A failed read renders the shared error marker inside the tab. Loading shows skeleton cards in the card grid, and current-page synchronization uses the shared ongoing loader. Retrying re-runs the lazy `list()` call without exposing transport details.
The Plugin list also shows synchronization failures on the current page. Its retry reapplies the latest client graph without changing Host enablement or refreshing the page.
@@ -61,7 +61,7 @@ The browser plugin registers one localized `settings.plugins.tab` contribution w
### Rendering
Row keys are scope-qualified (`global:`, `preset:<id>:<index>`), so one module appearing in both scopes keeps distinct disclosure state; a declared entry id appears in expanded details and supplies the collapsed subtitle after removal of a leading composition `include:` marker, while a row without one stays unlabeled. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision.
Row keys are scope-qualified (`global:`, `preset:<id>:<index>`), so one module appearing in both scopes keeps distinct disclosure state; a declared entry id appears in expanded details and, after removal of a leading composition `include:` marker, supplies the collapsed id chip when the result differs from the title, while a row without one stays unlabeled. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision.
</details>
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它先列 agent(智能体)预设分组,再列全局清单;两个分组默认都收起,按需展开,搜索期间保持展开。卡片显示可用的本地化标题与描述,以稳定的条目 id 标识实例,并展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会列出对应预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页处理加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。
**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它先列 agent(智能体)预设分组,再列全局清单;预设分组默认展开,全局分组默认收起,搜索期间两组都展开。卡片显示可用的本地化标题与描述,以稳定的条目 id 标识实例,并展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会列出对应预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页处理加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。
## 目录
@@ -31,9 +31,9 @@ kind: "package-reference"
### 阅读卡片
每张收起的卡片使用插件可用的本地化标题与描述。设置页会缩短字面的包名或模块名回退值,移除 npm scope 以及 `cordis:`、`cordis-plugin-` 或 `dsh-`/`dsh-host-`/`dsh-client-` 前缀;locale 标题保持原样。Host 在 `meta` 中提供 locale 与包字段的回退结果,Client 选择当前语言。详情与搜索仍保留完整模块标识。缺失的描述不显示,元信息诊断随卡片展示。这种短名显示仅用于设置页,不影响侧栏的插件页。
每张收起的卡片使用插件可用的本地化标题与描述;描述最多显示两行,展开卡片后显示全文。设置页会缩短字面的包名或模块名回退值,移除 npm scope 以及 `cordis:`、`cordis-plugin-` 或 `dsh-`/`dsh-host-`/`dsh-client-` 前缀;locale 标题保持原样。Host 在 `meta` 中提供 locale 与包字段的回退结果,Client 选择当前语言。详情与搜索仍保留完整模块标识。缺失的描述不显示,元信息诊断随卡片展示。这种短名显示仅用于设置页,不影响侧栏的插件页。
稳定的条目 id 显示在标题下方,卡片带有表示启停状态的小标签;彩色根 fiber 状态圆点只标记标签无法表达的阶段:`pending` 映射为 idle,`loading` 与 `unloading` 映射为 ongoing。`active` 或 `failed` fiber 只显示标签,不显示圆点。组合生成的次标题省略开头的 `include:` 标记;悬停、搜索、无障碍名称与展开详情仍保留完整 id。长条目 id 会在行内截断,悬停时仍可查看完整值。展开卡片后会显示声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-preset-registry/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设声明里的中文名。搜索按本地化标题与描述、模块名称及条目 id 过滤两组。
表示启停状态的小标签只标记与普通启用不同的状态:已停用、条件启用、由预设提供与启动失败;普通启用的行不带标签。彩色根 fiber 状态圆点标记没有标签表达的存活阶段:`pending` 映射为 idle,`loading` 与 `unloading` 映射为 ongoing;`active` 或 `failed` fiber 不显示圆点。稳定的条目 id 仅在与标题不同时显示,位于卡片左下角的代码标签中,与标题左对齐:组合生成的 id 在此省略开头的 `include:` 标记,省略后与标题相同的 id 不再重复显示。显示代码标签时,其悬停提示与卡片的无障碍名称带完整 id;搜索与展开详情始终保留完整 id。长条目 id 会在代码标签内截断,悬停时仍可查看完整值。展开卡片后会显示完整描述、声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-preset-registry/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设声明里的中文名。搜索按本地化标题与描述、模块名称及条目 id 过滤两组。
### 预设切换器
@@ -41,7 +41,7 @@ kind: "package-reference"
### 重试失败的读取
读取失败会在标签页内渲染共享 error 标记;加载与当前页面同步使用共享 ongoing loading。重试会重新执行懒 `list()` 调用,且不会暴露传输细节。
读取失败会在标签页内渲染共享 error 标记。加载时在卡片网格中显示骨架卡片,当前页面同步使用共享 ongoing loading。重试会重新执行懒 `list()` 调用,且不会暴露传输细节。
插件列表还显示当前页面的同步失败。重试会重新应用最新客户端图,不改变 Host 启用状态,也不刷新页面。
@@ -61,7 +61,7 @@ kind: "package-reference"
### 渲染
行 key 按作用域限定(`global:`、`preset:<id>:<index>`),因此同一模块出现在两个作用域时保持各自的展开状态;声明的条目 id 出现在展开详情中,并在去掉开头的组合 `include:` 标记后作为收起次标题,没有 id 的行不显示次级标签。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。
行 key 按作用域限定(`global:`、`preset:<id>:<index>`),因此同一模块出现在两个作用域时保持各自的展开状态;声明的条目 id 出现在展开详情中;去掉开头的组合 `include:` 标记后若与标题不同,则作为收起卡片的 id 代码标签,没有 id 的行不显示次级标签。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。
</details>
@@ -1,4 +1,6 @@
/* The tab's own width, not the window's, decides the card columns below. */
.section {
container: plugin-inventory / inline-size;
display: flex;
flex-direction: column;
gap: 14px;
@@ -7,7 +9,6 @@
color: var(--dsw-alias-label-primary);
}
.catalogHeading h3,
.status,
.failure p {
margin: 0;
@@ -85,37 +86,21 @@
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)) 18%, transparent);
}
.catalogHeading {
display: flex;
align-items: baseline;
gap: 7px;
padding: 0 2px;
}
.catalogHeading h3 {
font-size: 13px;
line-height: 20px;
font-weight: 600;
}
.catalogHeading span {
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
font-variant-numeric: tabular-nums;
}
.cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
/* Cards stretch to their grid row's height. The button fills a stretched card:
the title row and description stay at the top, in line with the other card's,
and the spare height sits above the entry id chip or at the bottom. */
.card {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
border: 0.5px solid var(--dsw-alias-settings-card-stroke);
@@ -127,11 +112,22 @@
border-color: var(--dsw-alias-border-l3);
}
/* The other card in an expanded card's row keeps its own height instead of
stretching alongside the details. The selectors assume the two columns
`.cards` declares, where each row starts at an odd position, and must change
with that count; in one column the matched card sits alone in its row. */
.card:nth-child(odd)[data-open='true'] + .card,
.card:nth-child(odd):has(+ .card[data-open='true']) {
align-self: start;
}
.cardContent {
box-sizing: border-box;
display: flex;
flex: 1 1 auto;
flex-direction: column;
align-items: stretch;
gap: 2px;
width: 100%;
min-height: 52px;
border: 0;
@@ -173,11 +169,35 @@
min-width: 0;
font-size: 14px;
line-height: 20px;
font-weight: 600;
font-weight: 500;
}
/* The Loader entry id keeps one size in the card subtitle and the row it expands into;
the collapsed subtitle stays on the quieter label tier, while the expanded row carries
/* Two lines at most while collapsed; the expanded card shows the full text. */
.cardDescription {
display: -webkit-box;
overflow: hidden;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
line-height: 18px;
text-wrap: pretty;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.card[data-open='true'] .cardDescription {
display: block;
}
/* The entry id is metadata, not prose: a code chip in the card's bottom-left
corner whose left edge lines up with the title. */
.cardMeta {
display: flex;
margin-top: auto;
padding-top: 6px;
}
/* The Loader entry id keeps one size in the card chip and the row it expands into;
the collapsed chip stays on the quieter label tier, while the expanded row carries
the card's primary value. */
.cardIdentity,
.entryValue {
@@ -188,15 +208,19 @@
.cardIdentity {
display: block;
width: 100%;
color: var(--dsw-alias-label-tertiary);
box-sizing: border-box;
max-width: 100%;
border-radius: var(--dsw-radius-xs);
padding: 1px 6px;
background: var(--dsw-alias-bg-module-platform);
color: var(--dsw-alias-label-secondary);
}
.cardTrailing {
display: inline-flex;
flex: none;
align-items: center;
gap: 7px;
gap: 8px;
color: var(--dsw-alias-label-tertiary);
}
@@ -213,11 +237,13 @@
gap: 10px;
}
/* Every group header takes the preset switcher's 36px height, so each subtitle
sits the same distance below its title. */
.groupTitleRow {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
min-height: 36px;
}
.group + .group {
@@ -266,7 +292,7 @@
.groupSub {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
row-gap: 4px;
margin: -6px 0 0 20px;
color: var(--dsw-alias-label-tertiary);
font-size: 12px;
@@ -274,6 +300,13 @@
font-variant-numeric: tabular-nums;
}
/* A middle dot with equal space on both sides separates the subtitle's parts. */
.groupSub > * + *::before {
content: '·';
padding: 0 6px;
color: var(--dsw-alias-label-tertiary);
}
.failedCount {
color: var(--dsw-alias-state-error-primary);
}
@@ -330,7 +363,7 @@
padding: 8px 10px;
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
color: var(--dsw-alias-state-error-primary);
font-size: 12.5px;
font-size: 12px;
line-height: 18px;
overflow-wrap: anywhere;
white-space: pre-line;
@@ -343,7 +376,7 @@
gap: 4px 8px;
margin: 0;
color: var(--dsw-alias-label-tertiary);
font-size: 12.5px;
font-size: 12px;
line-height: 18px;
}
@@ -353,10 +386,34 @@
background: transparent;
color: var(--dsw-alias-state-business-primary);
font: inherit;
font-size: 12.5px;
font-size: 12px;
cursor: pointer;
}
/* Loading placeholder in the cards grid, sized like a card with one description
line; its bars pulse through the shared skeleton token as the input-trigger
menu's pending rows do. */
.skeletonCard {
display: flex;
flex-direction: column;
gap: 8px;
border: 0.5px solid var(--dsw-alias-settings-card-stroke);
border-radius: var(--dsw-radius-xl);
padding: 15px 14px;
background: var(--dsw-alias-settings-card-fill);
}
.skeletonBar {
width: 40%;
height: 14px;
border-radius: var(--dsw-radius-xs);
background: var(--dsw-alias-bg-skeleton);
}
.skeletonBar + .skeletonBar {
width: 80%;
height: 12px;
}
.enabledIn {
display: flex;
@@ -430,9 +487,19 @@
.chevron {
transition: transform 140ms var(--ds-ease-in-out);
}
.skeletonBar {
animation: plugin-inventory-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;
}
}
@media (max-width: 680px) {
@keyframes plugin-inventory-skeleton {
0% { opacity: 1; }
40% { opacity: 0.6; }
80%, 100% { opacity: 1; }
}
@container plugin-inventory (max-width: 520px) {
.cards {
grid-template-columns: minmax(0, 1fr);
}
@@ -58,6 +58,9 @@ const PHASE_KEYS = {
unloading: 'unloading',
} satisfies Record<Exclude<PluginFiberPhase, null>, PluginInventoryLocaleKey>
/** Placeholder cards the loading skeleton lays out in the cards grid. */
const SKELETON_CARDS = [0, 1, 2, 3] as const
/** Localized accessible label for one root Fiber phase. */
function phaseLabel(phase: PluginFiberPhase, t: Translate): string {
return phase === null ? t('unobserved') : t(PHASE_KEYS[phase])
@@ -77,6 +80,16 @@ function entrySubtitle(entryId: string): string {
return entryId.replace(/^include:/, '')
}
/** Whether a card shows its entry id: the id exists and, without its `include:` marker, differs from the title. */
function idAddsToTitle(entryId: string | null, title: string): entryId is string {
return entryId !== null && entrySubtitle(entryId) !== title
}
/** Accessible card name: the title, the complete entry id when the card shows one, then the enablement state. */
function cardLabel(title: string, entryId: string | null, state: string): string {
return idAddsToTitle(entryId, title) ? `${title}, ${entryId}, ${state}` : `${title}, ${state}`
}
/** Preserve translated titles and shorten literal package or module name fallbacks in Settings. */
function pluginText(row: PluginInventoryEntry | AgentPresetRow, resolveText: PluginInventorySettingsTabInjected['resolveText']) {
const title = row.meta?.title
@@ -151,8 +164,12 @@ function PluginCard({
<IconChevronDownOutlineRegular className={css.chevron} size={12} aria-hidden="true" />
</span>
</span>
{description === undefined ? null : <span className={css.hint} id={descriptionId}>{description}</span>}
{entryId === null ? null : <code className={css.cardIdentity} title={entryId}>{entrySubtitle(entryId)}</code>}
{description === undefined ? null : <span className={css.cardDescription} id={descriptionId}>{description}</span>}
{idAddsToTitle(entryId, title) ? (
<span className={css.cardMeta}>
<code className={css.cardIdentity} title={entryId}>{entrySubtitle(entryId)}</code>
</span>
) : null}
</button>
{metadataError === undefined ? null : <p className={css.brokenNote} role="status" data-package-meta-error>{metadataError}</p>}
{open ? <div className={css.cardDetails} id={detailId}>{children}</div> : null}
@@ -189,15 +206,15 @@ function CardFacts({ moduleName, moduleLabel, entryId, facts }: {
/* `pending` is the only dotted phase with no work under way. `loading` and
* `unloading` are both live transitions the Host is running — an async
* disposer can hold `unloading` for a while — so both animate. `active` and
* `failed` carry no dot: a settled enabled row shows its enablement tag alone,
* and a failed row has the failure tag. */
* `failed` carry no dot: a settled enabled row needs no marker, and a failed
* row has the failure tag. */
const PHASE_DOT_STATES = {
pending: 'idle',
loading: 'ongoing',
unloading: 'ongoing',
} as const satisfies Partial<Record<NonNullable<PluginFiberPhase>, StateDotState>>
/** A live root-fiber phase whose dot still adds to the row's enablement tag. */
/** A live root-fiber phase the row marks with a dot. */
type DotPhase = keyof typeof PHASE_DOT_STATES
/** Whether a live root-fiber phase carries a dot of its own. */
@@ -220,16 +237,15 @@ function PhaseDot({ phase, t }: { readonly phase: DotPhase; readonly t: Translat
type EnablementKind = 'enabled' | 'disabled' | 'conditional' | 'preset' | 'failed'
const TAG_TONES = {
enabled: 'success',
disabled: 'neutral',
conditional: 'warning',
preset: 'info',
failed: 'danger',
} as const satisfies Record<EnablementKind, TagTone>
} as const satisfies Record<Exclude<EnablementKind, 'enabled'>, TagTone>
/** Enablement tag; `kind` selects the palette. */
/** Enablement tag for the states that depart from the default; a plainly enabled row carries none. */
function StateTag({ kind, label }: { readonly kind: EnablementKind; readonly label: string }): ReactNode {
return <Tag tone={TAG_TONES[kind]}>{label}</Tag>
return kind === 'enabled' ? null : <Tag tone={TAG_TONES[kind]}>{label}</Tag>
}
/** Render the read-only plugin inventory: agent presets first, then the global plane. */
@@ -296,8 +312,8 @@ export function PluginInventorySettingsTab(
const otherMatchCount = otherPresetMatches
.reduce((total, preset) => total + preset.rows.filter(rowMatch).length, 0)
// Both groups start collapsed; a search opens them for as long as it lasts.
const presetEffectiveOpen = searching || (presetOpen ?? false)
// The preset group starts open and the larger global plane folded; a search opens both for as long as it lasts.
const presetEffectiveOpen = searching || (presetOpen ?? true)
const globalEffectiveOpen = searching || (globalOpen ?? false)
const nothingMatches = searching && globalCount === 0 && selectedRows.length === 0
&& otherPresetMatches.length === 0
@@ -331,7 +347,7 @@ export function PluginInventorySettingsTab(
failed={failed}
expanded={expanded}
onToggle={toggleRow}
ariaLabel={`${title}${row.entryId === null ? '' : `, ${row.entryId}`}, ${stateText}`}
ariaLabel={cardLabel(title, row.entryId, stateText)}
trailing={(
<>
{row.enabled === true && showsPhaseDot(row.fiberPhase)
@@ -380,7 +396,7 @@ export function PluginInventorySettingsTab(
failed={failed}
expanded={expanded}
onToggle={toggleRow}
ariaLabel={`${title}, ${entry.entryId}, ${stateText}`}
ariaLabel={cardLabel(title, entry.entryId, stateText)}
trailing={(
<>
{entry.enabled && showsPhaseDot(entry.fiberPhase)
@@ -436,9 +452,15 @@ export function PluginInventorySettingsTab(
</div>
)}
{state.status === 'loading' ? (
<p className={`${css.status} ${css.statusWithDot}`} role="status">
<StateDot state="ongoing" />{t('loading')}
</p>
<div className={css.cards} role="status">
<span className={css.visuallyHidden}>{t('loading')}</span>
{SKELETON_CARDS.map(slot => (
<div key={slot} className={css.skeletonCard} aria-hidden="true">
<span className={css.skeletonBar} />
<span className={css.skeletonBar} />
</div>
))}
</div>
) : null}
{state.status === 'error' ? (
<div className={css.failure}>
@@ -506,9 +528,9 @@ export function PluginInventorySettingsTab(
</div>
</div>
<p className={css.groupSub}>
{t('presetSubtitle')}
<span>{t('presetSubtitle')}</span>
<span data-preset-plugin-count={selectedRows.length}>
{` · ${String(selectedRows.length)} ${t('countUnit')}`}
{`${String(selectedRows.length)} ${t('countUnit')}`}
</span>
</p>
{presetEffectiveOpen ? (
@@ -556,8 +578,8 @@ export function PluginInventorySettingsTab(
</button>
</div>
<p className={css.groupSub}>
{t('globalSubtitle')}
<span data-plugin-count={globalCount}>{` · ${String(globalCount)} ${t('countUnit')}`}</span>
<span>{t('globalSubtitle')}</span>
<span data-plugin-count={globalCount}>{`${String(globalCount)} ${t('countUnit')}`}</span>
{filteredFailed.length > 0 ? (
<span className={css.failedCount}>{filteredFailed.length} {t('failedCountLabel')}</span>
) : null}
@@ -76,7 +76,7 @@ export const en = {
enabledTag: 'Enabled',
disabledTag: 'Disabled',
conditionalTag: 'Conditional',
presetEnabledTag: 'Enabled via presets',
presetEnabledTag: 'Via presets',
failedTag: 'Failed',
moduleLabel: 'Module',
fromPreset: 'From',
@@ -128,7 +128,7 @@ describe('PluginInventorySettingsTab', () => {
? { entries: rows }
: { entries: [], agentPresets: [{ id: 'custom', isDefault: true, rows }] }
const view = await renderReady(snapshot)
fireEvent.click(scope === 'global' ? globalToggle() : presetToggle())
if (scope === 'global') fireEvent.click(globalToggle())
for (const [index, [moduleName, title]] of names.entries()) {
for (const fromManifest of [false, true]) {
const entryId = `include:short-${String(index)}-${String(fromManifest)}`
@@ -194,7 +194,6 @@ describe('PluginInventorySettingsTab', () => {
const view = render(<PluginInventorySettingsTab {...pageProps} />)
await screen.findByRole('searchbox', { name: en.search })
fireEvent.click(globalToggle())
fireEvent.click(presetToggle())
const global = screen.getByRole('button', { name: 'Navigation, include:global-navigation, Enabled' })
const preset = screen.getByRole('button', { name: 'Session runner, include:preset-runner, Enabled' })
expect(document.getElementById(global.getAttribute('aria-describedby')!)?.textContent).toBe('Global navigation controls')
@@ -256,7 +255,7 @@ describe('PluginInventorySettingsTab', () => {
const { locale, pageProps } = localizedProps(list)
const view = render(<PluginInventorySettingsTab {...pageProps} />)
await screen.findByRole('searchbox', { name: en.search })
fireEvent.click(scope === 'global' ? globalToggle() : presetToggle())
if (scope === 'global') fireEvent.click(globalToggle())
expect(screen.getByRole('button', { name: 'sidebar/navigation, include:navigation, Enabled' })).toBeTruthy()
expect(screen.getByText('Navigation description.')).toBeTruthy()
expect(screen.getByRole('button', { name: 'English title, include:commands, Enabled' })).toBeTruthy()
@@ -302,14 +301,14 @@ describe('PluginInventorySettingsTab', () => {
})
fireEvent.click(globalToggle())
expect(screen.getByText(en.metadataError.replace('{error}', error))).toBeTruthy()
const card = screen.getByRole('button', { name: 'legacy, include:legacy, Disabled' })
const card = screen.getByRole('button', { name: 'legacy, Disabled' })
expect(card).toHaveProperty('disabled', false)
expect(card.closest('li')?.getAttribute('data-failed')).toBeNull()
fireEvent.click(card)
expect(screen.getByText(en.moduleLabel).nextElementSibling?.textContent).toBe('@acme/dsh-legacy')
})
it('shows the default preset first with both groups collapsed', async () => {
it('shows the default preset first with its group open and the global plane folded', async () => {
const view = await renderReady()
const switcher = screen.getByRole('button', { name: en.switcherLabel })
@@ -325,18 +324,16 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.getByText(en.presetSubtitle)).toBeTruthy()
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6')
// Both groups start collapsed; opening the preset group lists its rows while the global plane stays folded.
expect(presetToggle().getAttribute('aria-expanded')).toBe('false')
expect(screen.queryAllByRole('listitem')).toHaveLength(0)
fireEvent.click(presetToggle())
// The preset group starts open with its rows while the larger global plane stays folded.
expect(presetToggle().getAttribute('aria-expanded')).toBe('true')
expect(screen.getAllByRole('listitem')).toHaveLength(6)
expect(screen.getAllByText(en.enabledTag)).toHaveLength(3)
// Only the states that depart from plainly enabled carry a tag.
expect(screen.queryByText(en.enabledTag)).toBeNull()
expect(screen.getByText(en.conditionalTag)).toBeTruthy()
expect(screen.getByText(en.disabledTag)).toBeTruthy()
expect(screen.getByText(en.failedTag)).toBeTruthy()
// The enablement tag is the row's one settled status signal: an active fiber
// and a row with no live fiber both render without a phase dot.
// A settled row carries no phase dot: an active fiber and a row with no live
// fiber both render without one.
expect(screen.queryByRole('img', { name: en.active })).toBeNull()
expect(screen.queryByRole('img', { name: en.unobserved })).toBeNull()
@@ -345,16 +342,16 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.getByText(`1 ${en.failedCountLabel}`)).toBeTruthy()
// A preset row expands into its source facts.
fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
expect(screen.getByText(en.fromPreset)).toBeTruthy()
expect(screen.getByText('标准模式')).toBeTruthy()
expect(screen.getByText(en.condition)).toBeTruthy()
expect(screen.getByText('process.platform === \'win32\'')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
expect(screen.queryByText(en.condition)).toBeNull()
// A failed preset row names its runtime state instead of a condition.
fireEvent.click(screen.getByRole('button', { name: 'crashy, crashy, Failed' }))
fireEvent.click(screen.getByRole('button', { name: 'crashy, Failed' }))
expect(screen.getByText(en.runtime)).toBeTruthy()
expect(screen.getByText('Failed to start')).toBeTruthy()
@@ -364,7 +361,7 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.getByText(en.moduleLabel).nextElementSibling?.textContent).toBe('@fixture/anonymous')
})
it('keeps the phase dot for a live phase the enablement tag does not state', async () => {
it('marks a live phase with a dot on an untagged enabled row', async () => {
await renderReady({
entries: [
{ entryId: 'booting', moduleName: '@fixture/booting', enabled: true, fiberPhase: 'loading' },
@@ -383,8 +380,8 @@ describe('PluginInventorySettingsTab', () => {
} as unknown as Snapshot)
fireEvent.click(globalToggle())
fireEvent.click(presetToggle())
expect(screen.getAllByText(en.enabledTag)).toHaveLength(6)
expect(screen.getAllByRole('listitem')).toHaveLength(6)
expect(screen.queryByText(en.enabledTag)).toBeNull()
expect(screen.getByRole('img', { name: en.loadingPhase })).toBeTruthy()
expect(screen.getByRole('img', { name: en.pending })).toBeTruthy()
expect(screen.getByRole('img', { name: en.unloading })).toBeTruthy()
@@ -407,7 +404,6 @@ describe('PluginInventorySettingsTab', () => {
}],
})
fireEvent.click(presetToggle())
expect(screen.getByRole('button', { name: 'tool-subagent, tool-subagent-primary, Enabled' })
.getAttribute('aria-expanded')).toBe('false')
const secondary = screen.getByRole('button', { name: `tool-subagent, ${longId}, Disabled` })
@@ -425,6 +421,25 @@ describe('PluginInventorySettingsTab', () => {
expect(filteredSecondary.getAttribute('aria-expanded')).toBe('true')
})
it('shows the entry id only where it adds to the title', async () => {
await renderReady({
entries: [
{ entryId: 'timer' as PluginEntryId, moduleName: 'cordis:timer', enabled: true, fiberPhase: 'active' },
{ entryId: 'include:hmr' as PluginEntryId, moduleName: '@deepseek-ai/cordis-plugin-hmr', enabled: true, fiberPhase: 'active' },
{ entryId: 'typert' as PluginEntryId, moduleName: '@fixture/typert-registry', enabled: true, fiberPhase: null },
],
})
fireEvent.click(globalToggle())
// An id equal to the title leaves both the card and its accessible name.
expect(screen.getByRole('button', { name: 'timer, Enabled' }).querySelector('code')).toBeNull()
// A composition id that matches the title once its include: marker drops is left out the same way.
expect(screen.getByRole('button', { name: 'hmr, Enabled' }).querySelector('code')).toBeNull()
const typert = screen.getByRole('button', { name: 'typert-registry, typert, Enabled' }).querySelector('code')
expect(typert?.textContent).toBe('typert')
expect(typert?.getAttribute('title')).toBe('typert')
})
it('expands the global plane with failures first and preset-provided rows inline', async () => {
const view = await renderReady()
@@ -439,13 +454,13 @@ describe('PluginInventorySettingsTab', () => {
// Rows the presets took over sit inline, marked instead of plainly disabled.
expect(screen.getAllByText(en.presetEnabledTag)).toHaveLength(2)
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Via presets' }))
expect(screen.getByText(en.presetProvidedDetail)).toBeTruthy()
expect(screen.getByText(en.enabledIn)).toBeTruthy()
expect(screen.getByText('标准模式 · ptc')).toBeTruthy()
// The failed global card reports its runtime state.
fireEvent.click(screen.getByRole('button', { name: 'telemetry, telemetry, Failed' }))
fireEvent.click(screen.getByRole('button', { name: 'telemetry, Failed' }))
expect(screen.getByText('Failed to start')).toBeTruthy()
// An enabled entry with no live fiber says so in its details, dot-free.
@@ -453,7 +468,7 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.getByText('Not running')).toBeTruthy()
// A disabled row outside every preset stays plainly disabled.
fireEvent.click(screen.getByRole('button', { name: 'dormant, dormant, Disabled' }))
fireEvent.click(screen.getByRole('button', { name: 'dormant, Disabled' }))
expect(screen.queryByText(en.presetProvidedDetail)).toBeNull()
fireEvent.click(globalToggle())
@@ -470,7 +485,6 @@ describe('PluginInventorySettingsTab', () => {
pickPreset('ptc')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('3')
fireEvent.click(presetToggle())
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash, Enabled' }))
// An unnamed preset labels its source by id.
expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('ptc')
@@ -480,18 +494,17 @@ describe('PluginInventorySettingsTab', () => {
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('0')
})
it('keeps the preset group folded until opened or searched', async () => {
it('folds the preset group on demand and reopens it for a search', async () => {
const view = await renderReady()
const toggle = presetToggle()
expect(toggle.getAttribute('aria-expanded')).toBe('true')
expect(view.container.querySelectorAll('[data-plugin-scope="preset"] li')).toHaveLength(6)
// The header keeps its count while the rows are folded away.
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6')
expect(view.container.querySelectorAll('[data-plugin-scope="preset"] li')).toHaveLength(0)
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('true')
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
fireEvent.change(screen.getByRole('searchbox', { name: en.search }), { target: { value: 'pwsh' } })
expect(toggle.getAttribute('aria-expanded')).toBe('true')
@@ -521,12 +534,11 @@ describe('PluginInventorySettingsTab', () => {
])
fireEvent.keyDown(document, { key: 'Escape' })
fireEvent.click(presetToggle())
fireEvent.click(screen.getByRole('button', { name: 'pwsh, pwsh, Conditional' }))
fireEvent.click(screen.getByRole('button', { name: 'pwsh, Conditional' }))
expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('Localized standard')
fireEvent.click(globalToggle())
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Via presets' }))
expect(screen.getByText('Localized standard · Localized ptc')).toBeTruthy()
})
@@ -536,7 +548,7 @@ describe('PluginInventorySettingsTab', () => {
fireEvent.click(screen.getByRole('menuitem', { name: 'ptc' }))
fireEvent.click(globalToggle())
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Enabled via presets' }))
fireEvent.click(screen.getByRole('button', { name: 'tool-bash, bash-host, Via presets' }))
fireEvent.click(screen.getByRole('button', { name: en.viewInPreset }))
expect(screen.getByRole('button', { name: en.switcherLabel }).textContent)
.toBe('标准模式 (default)')
@@ -588,10 +600,10 @@ describe('PluginInventorySettingsTab', () => {
expect(globalToggle().getAttribute('aria-expanded')).toBe('true')
expect(screen.getAllByRole('listitem')).toHaveLength(2)
fireEvent.click(screen.getByRole('button', { name: 'hmr, hmr, Enabled' }))
fireEvent.click(screen.getByRole('button', { name: 'hmr, Enabled' }))
expect(screen.getByText(en.runtime)).toBeTruthy()
expect(view.container.querySelector('[data-loader-entry]')?.textContent).toBe('hmr')
fireEvent.click(screen.getByRole('button', { name: 'off, off, Disabled' }))
fireEvent.click(screen.getByRole('button', { name: 'off, Disabled' }))
expect(screen.getAllByText(en.moduleLabel).length).toBeGreaterThan(0)
expect(screen.queryByText(en.runtime)).toBeNull()
})
@@ -608,8 +620,6 @@ describe('PluginInventorySettingsTab', () => {
expect(screen.queryByRole('button', { name: (name: string) => name.startsWith(en.globalTitle) })).toBeNull()
expect(screen.queryByText(en.empty)).toBeNull()
expect(screen.queryAllByRole('listitem')).toHaveLength(0)
fireEvent.click(presetToggle())
expect(screen.getAllByRole('listitem')).toHaveLength(1)
})
@@ -634,7 +644,8 @@ describe('PluginInventorySettingsTab', () => {
const deferred = Promise.withResolvers<Snapshot>()
const pending = render(<PluginInventorySettingsTab {...props(() => deferred.promise)} />)
expect(screen.getByText(en.loading).querySelector('[data-state="ongoing"]')).not.toBeNull()
// Loading shows skeleton cards; the status region announces the label as visually hidden text.
expect(screen.getByRole('status').textContent).toBe(en.loading)
pending.unmount()
await act(async () => { deferred.resolve(SNAPSHOT) })