From 3fc4142c04f5fd3f060266f5cb4c63dc870cb7d7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:17:34 +0800 Subject: [PATCH] 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. --- .../subagent-conversation/ui.expected.md | 4 ++-- apps/web/tests/subagent-conversation.e2e.ts | 2 +- .../src/client/SubagentCatalogAction.tsx | 6 ++++-- .../client/ui-subagent/src/client/locales.ts | 12 ++++++++---- .../ui-subagent/tests/conversation-ui.spec.tsx | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md index 3a9b03fffe..14b397d9b4 100644 --- a/apps/web/tests/snapshots/subagent-conversation/ui.expected.md +++ b/apps/web/tests/snapshots/subagent-conversation/ui.expected.md @@ -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] diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index 83ce78f3e4..904fcfcd09 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -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' }) diff --git a/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx b/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx index 359827780f..14f8169d82 100644 --- a/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx +++ b/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx @@ -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({ {descendants.running && } - {t('count.total', { count: descendantCount })} + {t(totalCountKey, { count: descendantCount })} {open && ( diff --git a/packages/client/ui-subagent/src/client/locales.ts b/packages/client/ui-subagent/src/client/locales.ts index 2ecf1be4f5..86562534b0 100644 --- a/packages/client/ui-subagent/src/client/locales.ts +++ b/packages/client/ui-subagent/src/client/locales.ts @@ -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 = { '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', diff --git a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx index 1687b1ffc1..5649b2b257 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx @@ -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() + + 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()