fix(ui-settings-plugin-inventory): address review on card ids and loading
The visible id chip and the accessible name now share one check for whether the entry id adds to the title, so an include: id that matches the title is left out of both. The loading skeleton's status region carries visually hidden text so screen readers announce it. The settings-chrome browser scenario expects the preset group open and enabled rows without a tag, and its two ARIA snapshots are refreshed. The row-partner CSS comment states its two-column assumption.
This commit is contained in:
1 parent
92690a5a3b
commit
e563bb4bbc
9 files changed
+51
-41
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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 67637808e7940157
|
||||
zh: d4d0716ea4d33e4a
|
||||
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/use-this-package/reading-a-card:
|
||||
en: 6ccd8a53e3205f24
|
||||
zh: 1d0ad59bfb7786e4
|
||||
en: 141d6a73e99fee51
|
||||
zh: 2aa28b174867c967
|
||||
/deepseek-ai-dsh-client-ui-settings-plugin-inventory/use-this-package/the-preset-switcher:
|
||||
en: 84e5938329199a22
|
||||
zh: 6f5cfaac56433e8b
|
||||
|
||||
@@ -33,7 +33,7 @@ A failed installation with pending pnpm build permissions offers **Allow these s
|
||||
|
||||
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.
|
||||
|
||||
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. The chip's hover text, search, and expanded details retain the complete id, and so does the accessible name unless the id equals the title. 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.
|
||||
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
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-reference"
|
||||
|
||||
每张收起的卡片使用插件可用的本地化标题与描述;描述最多显示两行,展开卡片后显示全文。设置页会缩短字面的包名或模块名回退值,移除 npm scope 以及 `cordis:`、`cordis-plugin-` 或 `dsh-`/`dsh-host-`/`dsh-client-` 前缀;locale 标题保持原样。Host 在 `meta` 中提供 locale 与包字段的回退结果,Client 选择当前语言。详情与搜索仍保留完整模块标识。缺失的描述不显示,元信息诊断随卡片展示。这种短名显示仅用于设置页,不影响侧栏的插件页。
|
||||
|
||||
表示启停状态的小标签只标记与普通启用不同的状态:已停用、条件启用、由预设提供与启动失败;普通启用的行不带标签。彩色根 fiber 状态圆点标记没有标签表达的存活阶段:`pending` 映射为 idle,`loading` 与 `unloading` 映射为 ongoing;`active` 或 `failed` fiber 不显示圆点。稳定的条目 id 仅在与标题不同时显示,位于卡片左下角的代码标签中,与标题左对齐:组合生成的 id 在此省略开头的 `include:` 标记,省略后与标题相同的 id 不再重复显示。代码标签的悬停提示、搜索与展开详情保留完整 id;除非 id 与标题相同,无障碍名称也保留完整 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 过滤两组。
|
||||
|
||||
### 预设切换器
|
||||
|
||||
|
||||
+3
-3
@@ -113,8 +113,9 @@
|
||||
}
|
||||
|
||||
/* The other card in an expanded card's row keeps its own height instead of
|
||||
stretching alongside the details. With two columns each row starts at an odd
|
||||
position; in one column the matched card sits alone in its row, unaffected. */
|
||||
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;
|
||||
@@ -414,7 +415,6 @@
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
|
||||
.enabledIn {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
+12
-7
@@ -80,9 +80,14 @@ function entrySubtitle(entryId: string): string {
|
||||
return entryId.replace(/^include:/, '')
|
||||
}
|
||||
|
||||
/** Accessible card name: the title, the entry id unless it repeats the title, then the enablement state. */
|
||||
/** 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 [title, ...entryId === null || entryId === title ? [] : [entryId], state].join(', ')
|
||||
return idAddsToTitle(entryId, title) ? `${title}, ${entryId}, ${state}` : `${title}, ${state}`
|
||||
}
|
||||
|
||||
/** Preserve translated titles and shorten literal package or module name fallbacks in Settings. */
|
||||
@@ -135,7 +140,6 @@ function PluginCard({
|
||||
const open = expanded === rowKey
|
||||
const detailId = `plugin-details-${encodeURIComponent(rowKey)}`
|
||||
const descriptionId = useId()
|
||||
const subtitle = entryId === null ? null : entrySubtitle(entryId)
|
||||
return (
|
||||
<li
|
||||
className={css.card}
|
||||
@@ -161,11 +165,11 @@ function PluginCard({
|
||||
</span>
|
||||
</span>
|
||||
{description === undefined ? null : <span className={css.cardDescription} id={descriptionId}>{description}</span>}
|
||||
{entryId === null || subtitle === title ? null : (
|
||||
{idAddsToTitle(entryId, title) ? (
|
||||
<span className={css.cardMeta}>
|
||||
<code className={css.cardIdentity} title={entryId}>{subtitle}</code>
|
||||
<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}
|
||||
@@ -448,7 +452,8 @@ export function PluginInventorySettingsTab(
|
||||
</div>
|
||||
)}
|
||||
{state.status === 'loading' ? (
|
||||
<div className={css.cards} role="status" aria-label={t('loading')}>
|
||||
<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} />
|
||||
|
||||
@@ -301,7 +301,7 @@ 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)
|
||||
@@ -433,8 +433,8 @@ describe('PluginInventorySettingsTab', () => {
|
||||
|
||||
// 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 matches the title once its include: marker drops; the accessible name keeps it whole.
|
||||
expect(screen.getByRole('button', { name: 'hmr, include:hmr, 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')
|
||||
@@ -644,8 +644,8 @@ describe('PluginInventorySettingsTab', () => {
|
||||
|
||||
const deferred = Promise.withResolvers<Snapshot>()
|
||||
const pending = render(<PluginInventorySettingsTab {...props(() => deferred.promise)} />)
|
||||
// Loading shows skeleton cards named by the status region, with no visible text.
|
||||
expect(screen.getByRole('status', { name: en.loading }).textContent).toBe('')
|
||||
// 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) })
|
||||
|
||||
|
||||
Reference in new issue
Block a user