fix(ui): show subagent descriptions above names without arrows (#7730)

* fix(ui): show subagent descriptions above names without disclosure arrows

* fix(ui): narrow subagent result cards

* Revert "fix(ui): narrow subagent result cards"

This reverts commit 23a5a353ac9daf0e322f28e19c9d2c2cfbb15acb.

* test(web): sample agent row geometry in one browser task
This commit is contained in:
Dudu-0223 authored and GitHub committed 2026-11-07 01:03:59 +08:00
1 parent 2d463af8d2
commit e89d2b031f
18 files changed
+43 -109

No files matched your search

+10
View File
@@ -62,6 +62,16 @@ describe.skipIf(MODE === 'record')('web e2e: compact Tool details', () => {
expect(await row.locator('[class*="ioCard"]').count()).toBe(0)
expect(await row.getByRole('button', { name: 'Inspect', exact: true }).count()).toBe(1)
}
const agentRow = page.locator('[data-tool="list_agents"]').getByRole('listitem').first()
expect(await agentRow.getByRole('button').count()).toBe(0)
expect(await agentRow.locator('svg').count()).toBe(0)
// Read both positions in one browser task so scrolling cannot move the viewport between measurements.
const agentLayout = await agentRow.evaluate((element) => {
const description = element.querySelector('[class*="heading"] > span')!.getBoundingClientRect()
const name = element.querySelector('[class*="subtitle"]')!.getBoundingClientRect()
return { descriptionBottom: description.bottom, nameTop: name.top }
})
expect(agentLayout.descriptionBottom).toBeLessThanOrEqual(agentLayout.nameTop)
const changedTodos = page.locator('[data-tool="todo_write"]').nth(1)
await changedTodos.getByText('1 added · 1 updated · 1 removed', { exact: true }).waitFor()
for (const summary of await page.locator('[data-tool] details > summary').all()) await summary.click()
-2
View File
@@ -33,8 +33,6 @@ export const de = {
'more': 'Mehr',
'collapse': 'Einklappen',
'expand': 'Ausklappen',
'subtitle.expand': 'Beschreibung für {name} ausklappen',
'subtitle.collapse': 'Beschreibung für {name} einklappen',
'back': 'Zurück',
'brand.localBuild': 'Lokaler DSH-Build',
'workspace.defaultName': 'Standardarbeitsbereich',
-2
View File
@@ -32,8 +32,6 @@ export const en = {
'more': 'More',
'collapse': 'Collapse',
'expand': 'Expand',
'subtitle.expand': 'Expand description for {name}',
'subtitle.collapse': 'Collapse description for {name}',
'back': 'Back',
'brand.localBuild': 'DSH Local Build',
'workspace.defaultName': 'Default workspace',
-2
View File
@@ -33,8 +33,6 @@ export const es = {
'more': 'Más',
'collapse': 'Contraer',
'expand': 'Expandir',
'subtitle.expand': 'Expandir la descripción de {name}',
'subtitle.collapse': 'Contraer la descripción de {name}',
'back': 'Atrás',
'brand.localBuild': 'Compilación local de DSH',
'workspace.defaultName': 'Espacio de trabajo predeterminado',
-2
View File
@@ -33,8 +33,6 @@ export const fr = {
'more': 'Plus',
'collapse': 'Réduire',
'expand': 'Développer',
'subtitle.expand': 'Développer la description de {name}',
'subtitle.collapse': 'Réduire la description de {name}',
'back': 'Retour',
'brand.localBuild': 'Version locale de DSH',
'workspace.defaultName': 'Espace de travail par défaut',
-2
View File
@@ -33,8 +33,6 @@ export const ja = {
'more': 'その他',
'collapse': '折りたたむ',
'expand': '展開',
'subtitle.expand': '{name} の説明を展開',
'subtitle.collapse': '{name} の説明を折りたたむ',
'back': '戻る',
'brand.localBuild': 'DSHローカルビルド',
'workspace.defaultName': 'デフォルトワークスペース',
-2
View File
@@ -33,8 +33,6 @@ export const pt = {
'more': 'Mais',
'collapse': 'Recolher',
'expand': 'Expandir',
'subtitle.expand': 'Expandir descrição de {name}',
'subtitle.collapse': 'Recolher descrição de {name}',
'back': 'Voltar',
'brand.localBuild': 'Versão local do DSH',
'workspace.defaultName': 'Espaço de trabalho padrão',
-2
View File
@@ -33,8 +33,6 @@ export const ru = {
'more': 'Ещё',
'collapse': 'Свернуть',
'expand': 'Развернуть',
'subtitle.expand': 'Развернуть описание {name}',
'subtitle.collapse': 'Свернуть описание {name}',
'back': 'Назад',
'brand.localBuild': 'Локальная сборка DSH',
'workspace.defaultName': 'Рабочая область по умолчанию',
-2
View File
@@ -30,8 +30,6 @@ export const zh = {
'more': '更多',
'collapse': '收起',
'expand': '展开',
'subtitle.expand': '展开{name}的描述',
'subtitle.collapse': '收起{name}的描述',
'back': '返回',
'brand.localBuild': 'DSH 本地构建',
'workspace.defaultName': '默认工作区',
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-tool/use-this-package:
en: 09231e7cc6515a01
zh: c306bb141cf9bf9d
en: 4d8cb38024095705
zh: 1baf6fa42f3fb80f
/deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view:
en: e458aff61e634190
zh: 650e417f450b3edf
+1 -1
View File
@@ -25,7 +25,7 @@ English | [中文](README.zh.md)
<a id="use-this-package"></a>
## Use this package
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display the agent name supplied by the recorded call, or the agent ID in historical calls. Text agent lists show each agent name, or the historical ID, above a single-line description, with compact spacing and recorded status. Click the description to expand or collapse its full text; its accessible name follows the selected locale.
Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display the agent name supplied by the recorded call, or the agent ID in historical calls. Agent lists show the full description above the name, or the historical ID, with compact spacing and recorded status. Both lines wrap as needed and have no disclosure arrows; rows without a description show only the name.
Settled foreground commands with a nonzero exit code, a terminating signal, or an explicit missing exit code keep their description in the ordinary gray text and show a neutral capsule with a red dot immediately after it. The capsule displays only the recorded code, signal, or localized missing-code status and stays on the same line; the description truncates with an ellipsis to reserve its space. Running, successful, and background calls omit it. Generic execution errors and stopped summaries retain their error and warning colors, including on hover.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示记录的调用所提供的智能体名称,历史调用则显示智能体 ID。文本形式的智能体列表以紧凑间距展示智能体名称(历史记录为 ID)、下方的单行描述及记录中的状态。点击描述可展开或收起全文;其无障碍名称使用当前语言。
工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示记录的调用所提供的智能体名称,历史调用则显示智能体 ID。智能体列表以紧凑间距在上方展示完整描述,下方展示名称(历史记录为 ID),并保留记录中的状态。两行均按需换行,不带展开箭头;没有描述时只显示名称。
已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。
@@ -188,42 +188,6 @@
padding: 6px 10px 4px;
}
.subtitleToggle {
display: flex;
align-items: flex-start;
gap: 4px;
width: 100%;
padding: 0;
border: 0;
background: transparent;
color: var(--dsw-alias-label-tertiary);
font: inherit;
font-size: 12px;
text-align: left;
cursor: pointer;
}
.subtitleToggle > span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.subtitleToggle > svg {
flex: none;
margin-top: 3px;
}
.subtitleToggle[aria-expanded='true'] > span {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.subtitleToggle[aria-expanded='true'] > svg {
transform: rotate(90deg);
}
.description {
margin: 6px 0;
color: var(--dsw-alias-label-caption);
@@ -1,5 +1,4 @@
/** Compact, read-only fields and lists for recorded Tool results. */
import { useState } from 'react'
import {
CodeBlock, MarkdownText, IconCheckOutlineRegular, IconChevronRightOutlineRegular, IconPlayOutlineRegular,
} from '@deepseek-ai/dsh-client-ui-primitives'
@@ -28,7 +27,7 @@ export interface ToolDetailItem {
/** Localized display data shared by the built-in detail cards. */
export interface ToolDetailsModel {
items: readonly ToolDetailItem[]
/** Tight rows with single-line subtitles that can expand in place. */
/** Tight spacing for recorded entity rows. */
compact?: boolean
summary?: string
/** Header text while expanded, omitting a receipt status shown in the body. */
@@ -43,9 +42,8 @@ interface DetailContentProps {
onOpenFile?: ((path: string, options?: OpenFileOptions) => void) | undefined
}
function DetailItem({ item, compact, t, onOpenFile }: { item: ToolDetailItem; compact?: boolean | undefined } & DetailContentProps) {
function DetailItem({ item, t, onOpenFile }: { item: ToolDetailItem } & DetailContentProps) {
const status = item.status
const [expanded, setExpanded] = useState(false)
return (
<li className={css.item} data-change={item.change?.value}>
{item.title !== undefined && (
@@ -77,19 +75,7 @@ function DetailItem({ item, compact, t, onOpenFile }: { item: ToolDetailItem; co
)}
</div>
)}
{item.subtitle !== undefined && (compact ? (
<button
type="button"
className={css.subtitleToggle}
aria-expanded={expanded}
aria-label={item.title === undefined
? t(expanded ? 'collapse' : 'expand')
: t(expanded ? 'subtitle.collapse' : 'subtitle.expand', { name: item.title })}
onClick={() => { setExpanded(value => !value) }}
>
<span>{item.subtitle}</span><IconChevronRightOutlineRegular size={12} />
</button>
) : <div className={css.subtitle}>{item.subtitle}</div>)}
{item.subtitle !== undefined && <div className={css.subtitle}>{item.subtitle}</div>}
{item.description !== undefined && <p className={css.description}>{item.description}</p>}
{item.fields.length > 0 && (
<dl className={css.fields}>
@@ -144,7 +130,7 @@ export function ToolDetails({ model, hasInspect = false, t, onOpenFile }: {
{model.caption !== undefined && <div className={css.caption}>{model.caption}</div>}
{model.items.length === 0 ? <p className={css.empty}>{model.empty}</p> : (
<ul className={css.list}>
{model.items.map((item, index) => <DetailItem key={index} item={item} compact={model.compact} t={t} onOpenFile={onOpenFile} />)}
{model.items.map((item, index) => <DetailItem key={index} item={item} t={t} onOpenFile={onOpenFile} />)}
</ul>
)}
{model.unchanged !== undefined && (
@@ -29,7 +29,14 @@ function teamTaskItems(value: unknown, t: DetailTranslate): DetailItem[] {
}
function agentList(text: string, json: unknown, t: DetailTranslate): ToolDetailsModel | null {
if (Array.isArray(json)) return detailList(inspectionItems(json, t), t('detail.agents.count', { count: json.length }), t)
if (Array.isArray(json)) {
const items = inspectionItems(json, t).map((item) => {
if (item.title === undefined || item.description === undefined) return item
const { title, description, ...rest } = item
return { ...rest, title: description, subtitle: title }
})
return { ...detailList(items, t('detail.agents.count', { count: json.length }), t), compact: true }
}
if (text === '(no subagents)') return detailList([], t('detail.agents.count', { count: 0 }), t)
const items: DetailItem[] = []
for (const line of text.split('\n')) {
@@ -39,7 +46,7 @@ function agentList(text: string, json: unknown, t: DetailTranslate): ToolDetails
const [, agent, state, title] = match
if (agent === undefined || state === undefined) return null
items.push({
title: agent, ...(title === undefined ? {} : { subtitle: title }),
title: title || agent, ...(title ? { subtitle: agent } : {}),
badge: detailBadge(state, t),
fields: [],
})
@@ -93,20 +93,20 @@ describe('control detail adapters', () => {
const agents = details('list_agents', 'lead [running] — root agent\nreviewer/checker [inactive] — Check tests\nold-id [unavailable]')
expect(agents?.summary).toBe('3 agents')
expect(agents?.items).toEqual([
{ title: 'lead', subtitle: 'root agent', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'reviewer/checker', subtitle: 'Check tests', badge: { label: 'Inactive', tone: 'warning' }, fields: [] },
{ title: 'root agent', subtitle: 'lead', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'Check tests', subtitle: 'reviewer/checker', badge: { label: 'Inactive', tone: 'warning' }, fields: [] },
{ title: 'old-id', badge: { label: 'Unavailable', tone: 'neutral' }, fields: [] },
])
const historical = details('list_agents', 'a1 [running] — Build\na2 [ready] parent=a1 depth=1 — Review\na3 [diagnostic: corrupt] parent=a1 depth=1', { scope: 'descendants' })
expect(historical?.summary).toBe('3 agents')
expect(historical?.items).toEqual([
{ title: 'a1', subtitle: 'Build', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'a2', subtitle: 'Review', badge: { label: 'Ready', tone: 'neutral' }, fields: [] },
{ title: 'Build', subtitle: 'a1', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'Review', subtitle: 'a2', badge: { label: 'Ready', tone: 'neutral' }, fields: [] },
{ title: 'a3', badge: { label: 'diagnostic: corrupt', tone: 'neutral' }, fields: [] },
])
const structured = details('list_agents', JSON.stringify([{ name: 'lead', status: 'running', description: 'root agent' }, { name: 'reviewer', status: 'inactive' }]))
expect(structured?.items).toEqual([
{ title: 'lead', description: 'root agent', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'root agent', subtitle: 'lead', badge: { label: 'Running', tone: 'info' }, fields: [] },
{ title: 'reviewer', badge: { label: 'Inactive', tone: 'warning' }, fields: [] },
])
expect(details('list_agents', 'reviewer running')).toBeNull()
@@ -153,26 +153,17 @@ describe('todosDetail', () => {
})
describe('DetailsRow', () => {
it('puts agent names first and lets a compact description expand and collapse', () => {
it('shows the full agent description above its name without a disclosure', () => {
const description = 'Verify every construction bound. '.repeat(20)
const block = { ...result('list_agents', null), content: [{ type: 'text' as const, text: `reviewer/checker [running] — ${description}` }] }
const model = detailsCardModel(block, t, 'en')!
render(<ToolDetails model={model} t={t} />)
const name = screen.getByText('reviewer/checker')
const toggle = screen.getByRole('button', { name: 'Expand description for reviewer/checker' })
expect(name.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(toggle.textContent).toBe(description)
fireEvent.click(toggle)
expect(screen.getByRole('button', { name: 'Collapse description for reviewer/checker' }).getAttribute('aria-expanded')).toBe('true')
fireEvent.click(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
})
it('uses an unqualified accessible name when a compact description has no title', () => {
render(<ToolDetails model={{ compact: true, items: [{ subtitle: 'Details', fields: [] }] }} t={t} />)
const toggle = screen.getByRole('button', { name: 'Expand' })
fireEvent.click(toggle)
expect(screen.getByRole('button', { name: 'Collapse' })).toBe(toggle)
const title = screen.getByText(description.trim())
expect(title.compareDocumentPosition(name) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(screen.getByRole('listitem').textContent).toBe(`${description}Runningreviewer/checker`)
expect(screen.queryByRole('button')).toBeNull()
expect(screen.getByRole('listitem').querySelector('svg')).toBeNull()
})
it('expands fields with the keyboard and keeps Inspect available', () => {
+5 -13
View File
@@ -477,15 +477,9 @@
- button "List subagents 3 agents" [expanded]
- text: Recorded result
- list:
- listitem:
- text: agent-demo-review Running
- button "Expand description for agent-demo-review": 检查工具卡片布局
- listitem:
- text: agent-demo-docs Idle
- button "Expand description for agent-demo-docs": 整理工具展示清单
- listitem:
- text: agent-demo-icons Ready
- button "Expand description for agent-demo-icons": 检查状态图标
- listitem: 检查工具卡片布局 Running agent-demo-review
- listitem: 整理工具展示清单 Idle agent-demo-docs
- listitem: 检查状态图标 Ready agent-demo-icons
- button "Inspect"
- button "Send message agent-demo-review" [expanded]
- text: Recorded result
@@ -658,8 +652,7 @@
- term: Role
- definition: lead
- listitem:
- text: ui-review Running
- paragraph: 检查工具卡片的可读性
- text: 检查工具卡片的可读性 Running ui-review
- term: ID
- definition: agent-demo-review
- term: Role
@@ -669,8 +662,7 @@
- term: Context
- definition: fresh
- listitem:
- text: docs Inactive
- paragraph: 整理工具清单
- text: 整理工具清单 Inactive docs
- term: ID
- definition: agent-demo-docs
- term: Role