fix(web): pluralize singular subagent counts

The localized catalog exposed one count.total and one count.running string for every cardinality. The English dictionary therefore rendered both the visible trigger and its accessibility label as 1 subagents, and the assembled Web golden had begun preserving that grammar error.

Split both count families into explicit one and other keys, following the existing client locale convention. SubagentCatalogAction selects the pair from the effective descendant count; English uses subagent for one and subagents otherwise, while Chinese keeps its unchanged classifier text under the same key domain.

Add a component regression proving a single running descendant selects both singular keys. Update the real Web E2E locator and keyless assembled aria golden from 1 subagents to 1 subagent. Both ui-subagent test files pass all 28 tests and the package TypeScript project builds cleanly.
This commit is contained in:
Tianyi Cui
2026-08-02 20:17:34 +08:00
parent ac88b6e4c7
commit 3fc4142c04
5 changed files with 33 additions and 9 deletions

View File

@@ -3,8 +3,8 @@
- button "Ask a research subagent to"
- text: /
- button "event-sourcing researcher" [disabled]
- button "1 subagents":
- text: 1 subagents
- button "1 subagent":
- text: 1 subagent
- img
- tablist:
- tab "Chat" [selected]

View File

@@ -331,7 +331,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
it('opens an unavailable persisted grandchild after recording the available child', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-grandchild'))
await page.getByRole('button', { name: '1 subagents' }).click()
await page.getByRole('button', { name: '1 subagent' }).click()
await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).click()
await page.getByText('The parent session is offline; reopen it to continue sending messages.').waitFor()
const hierarchy = page.getByRole('navigation', { name: 'Session hierarchy' })

View File

@@ -324,6 +324,8 @@ export function SubagentCatalogAction({
// The catalog can arrive before the session-list baseline; never undercount
// the already-visible direct rows during that short bootstrap window.
const descendantCount = Math.max(healthy.length, descendants.count)
const totalCountKey = descendantCount === 1 ? 'count.total.one' : 'count.total.other'
const runningCountKey = descendantCount === 1 ? 'count.running.one' : 'count.running.other'
const observeCatalog = (parentSessionId: SessionId, next: boolean): void => {
if (next) observedCatalogs.current.add(parentSessionId)
@@ -432,7 +434,7 @@ export function SubagentCatalogAction({
className={css.trigger}
aria-haspopup="tree"
aria-expanded={open}
aria-label={t(descendants.running ? 'count.running' : 'count.total', { count: descendantCount })}
aria-label={t(descendants.running ? runningCountKey : totalCountKey, { count: descendantCount })}
onClick={() => { changeOpen(!open) }}
onKeyDown={(event) => {
if (event.key !== 'ArrowDown') return
@@ -444,7 +446,7 @@ export function SubagentCatalogAction({
<span className={css.activitySlot}>
{descendants.running && <StateDot state="ongoing" />}
</span>
<span className={css.count}>{t('count.total', { count: descendantCount })}</span>
<span className={css.count}>{t(totalCountKey, { count: descendantCount })}</span>
<IconChevronDownOutline14 className={open ? css.triggerOpen : undefined} />
</button>
{open && (

View File

@@ -24,8 +24,10 @@ export const zh = {
'activity.inactive': '当前未运行',
'branch.collapse': '收起 {label} 的下级子代理',
'branch.expand': '展开 {label} 的下级子代理',
'count.total': '{count} 个子代理',
'count.running': '{count} 个子代理,正在运行',
'count.total.one': '{count} 个子代理',
'count.total.other': '{count} 个子代理',
'count.running.one': '{count} 个子代理,正在运行',
'count.running.other': '{count} 个子代理,正在运行',
'tree.aria': '子代理会话',
'readonly.oneShot.title': '一次性子代理记录',
'readonly.title': '此子代理暂时只读',
@@ -54,8 +56,10 @@ export const en: Record<SubagentKey, string> = {
'activity.inactive': 'not running',
'branch.collapse': 'Collapse {label} descendants',
'branch.expand': 'Expand {label} descendants',
'count.total': '{count} subagents',
'count.running': '{count} subagents running',
'count.total.one': '{count} subagent',
'count.total.other': '{count} subagents',
'count.running.one': '{count} subagent running',
'count.running.other': '{count} subagents running',
'tree.aria': 'Subagent sessions',
'readonly.oneShot.title': 'One-shot subagent record',
'readonly.title': 'This subagent is read-only for now',

View File

@@ -167,6 +167,24 @@ describe('SubagentCatalogAction', () => {
expect(input.setCatalogOpen).toHaveBeenLastCalledWith(PARENT, false)
})
it('selects singular count keys for one descendant', () => {
const base = props(catalog({
entries: [{
kind: 'child', id: CHILD, mode: 'continuable', label: 'worker',
activity: 'running', hasChildren: false,
}],
}), {}, {
[CHILD]: {
...summary(CHILD, Date.now()), parentId: PARENT, origin: 'subagent', running: true,
},
})
const translate = vi.fn(base.t)
render(<SubagentCatalogAction {...base} t={translate} />)
expect(translate).toHaveBeenCalledWith('count.running.one', { count: 1 })
expect(translate).toHaveBeenCalledWith('count.total.one', { count: 1 })
})
it('supports trigger/menu keyboard traversal, Escape focus restore, and outside close', async () => {
const input = props(catalog())
render(<SubagentCatalogAction {...input} />)