diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
index ac7fc12150..f93d3a4a34 100644
--- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
-2026-07-27-web-subagent-conversations.md: 60717365244ff3ca3ebc4a400b15bfc81062212c
-2026-07-27-web-subagent-conversations.zh.md: 217ed44249c0d1beb731e24e52054f200fae4b9b
+2026-07-27-web-subagent-conversations.md: 1b9bbfbc566e77932629af9f1e0714819973f111
+2026-07-27-web-subagent-conversations.zh.md: a9ed0e20f3617f7f5f8ae9d0f2d77c7ba8aef9ea
diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
index 6071736524..1b9bbfbc56 100644
--- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
+++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md
@@ -37,7 +37,7 @@ The Figma [subagent list](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Ha
## Product contract
-The header action is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. Its trigger counts every known session-summary descendant reached through an uninterrupted `origin: 'subagent'` lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. Because ordinary sidebar rows hide subagent-origin sessions, the Workspace browser indexes the same uninterrupted lineage onto each visible ordinary row: any running descendant supplies its blue activity indicator and exact count in hover and assistive text without describing an idle parent as running. An ordinary fork starts a separate aggregation subtree. Parent running and pending interaction remain distinct primary statuses; descendant activity becomes a second hover and assistive status when either is present. Every healthy direct-catalog row carries a read-time `hasChildren` hint derived only from direct lineage headers with durable `origin: 'subagent'`; normal healthy and diagnostic subagent candidates carry that marker, while ordinary forks do not. This lookahead reads no descendant event log, and the descriptor-backed catalog loaded after disclosure remains authoritative. When summaries establish descendants before that catalog exists or after a stale empty response, the action stays visible and exposes only disabled loading rows until opening it refreshes the catalog; summary-only rows never grant navigation. The UI omits disclosure for a known leaf before interaction; the hint does not promise that the child will remain a leaf. While an expanded direct catalog is loading, known lineage reserves one disabled loading row per direct descendant without recursively fetching descendant catalogs. The tree then presents continuable and one-shot rows, falling back to the session id when an optional one-shot label is absent. Corrupt, unsupported, and unavailable candidates remain visible as disabled diagnostic rows.
+The header action is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. Its trigger counts every known session-summary descendant reached through an uninterrupted `origin: 'subagent'` lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. Because ordinary sidebar rows hide subagent-origin sessions, the Workspace browser indexes the same uninterrupted lineage onto each visible ordinary row: any running descendant supplies its blue activity indicator and exact count in hover and assistive text without describing an idle parent as running. An ordinary fork starts a separate aggregation subtree. Pending interaction outranks parent running; either remains primary while descendant activity becomes a second hover and assistive status. With neither present, descendant activity outranks an unviewed completion reminder, which returns after the last running descendant stops. Every healthy direct-catalog row carries a read-time `hasChildren` hint derived only from direct lineage headers with durable `origin: 'subagent'`; normal healthy and diagnostic subagent candidates carry that marker, while ordinary forks do not. This lookahead reads no descendant event log, and the descriptor-backed catalog loaded after disclosure remains authoritative. When summaries establish descendants before that catalog exists or after a stale empty response, the action stays visible and exposes only disabled loading rows until opening it refreshes the catalog; summary-only rows never grant navigation. The UI omits disclosure for a known leaf before interaction; the hint does not promise that the child will remain a leaf. While an expanded direct catalog is loading, known lineage reserves one disabled loading row per direct descendant without recursively fetching descendant catalogs. The tree then presents continuable and one-shot rows, falling back to the session id when an optional one-shot label is absent. Corrupt, unsupported, and unavailable candidates remain visible as disabled diagnostic rows.
`running` means the exact child Agent driver is draining work at the Host sampling boundary; `inactive` means that driver is idle or absent. The UI does not translate either value into success, failure, cancellation, completeness, or resumability. `subagent.list` supplies the current driver-status baseline, `host/session-status` updates known activity in place, request-local replay prevents an older in-flight list response from overwriting a newer transition, and `host/session-removed` returns a known row to `inactive`; reconnect reads a fresh baseline. A `host/session-added` frame for a direct subagent immediately flips any loaded parent row to `hasChildren: true`, and that positive hint survives an older in-flight catalog response; membership, labels, mode, diagnostics, and the authoritative snapshot still require a debounced `subagent.list` refresh while the affected branch is open. A prompt response remains delivery-time authority.
diff --git a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
index 217ed44249..a9ed0e20f3 100644
--- a/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
@@ -37,7 +37,7 @@ Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5
## 产品契约
-只有当完整的直接目录空响应与会话摘要投影相符,二者均表明没有已知的 subagent 后代时,才不显示页头操作。其触发器会统计经不间断的 `origin: 'subagent'` 谱系可达的每个已知会话摘要后代,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。由于普通侧边栏行会隐藏 origin 为 subagent 的会话,Workspace 浏览器会在每个可见的普通行上索引同一条不间断谱系:任何运行中的后代都会让该行显示蓝色活动指示器,并在悬停与无障碍文本中给出确切数量,同时不会把空闲 parent 描述为正在运行。普通 fork 会开启单独的聚合子树。parent 的运行中状态与待处理交互仍是彼此不同的主要状态;只要其中任一存在,后代活动就成为悬停与无障碍状态中的第二项。每个健康的直接目录行都携带读取时的 `hasChildren` 提示,该值只根据持久化 `origin: 'subagent'` 的直接谱系 header 派生;正常的健康与 diagnostic subagent 候选都会携带该标记,而普通 fork 不会。该预查不读取任何后代事件日志,展开后仍以描述符支撑的目录为权威依据。当摘要在该目录尚不存在时或在一次陈旧的空响应后确认已有后代时,该操作会保持可见,并且在打开它以刷新目录之前仅显示禁用的加载行;仅由摘要支撑的行绝不会提供导航能力。UI 会在交互前就省略已知叶子节点的展开控件;该提示不承诺 child 会一直是叶子。已展开的直接目录加载期间,已知谱系会为每个直接后代预留一行禁用的加载行,而不会递归获取后代目录。随后树会呈现可继续与 one-shot 行;one-shot 的可选 label 缺失时,回退到其会话 id。损坏、不受支持或不可用的候选仍以禁用的 diagnostic 行显示。
+只有当完整的直接目录空响应与会话摘要投影相符,二者均表明没有已知的 subagent 后代时,才不显示页头操作。其触发器会统计经不间断的 `origin: 'subagent'` 谱系可达的每个已知会话摘要后代,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。由于普通侧边栏行会隐藏 origin 为 subagent 的会话,Workspace 浏览器会在每个可见的普通行上索引同一条不间断谱系:任何运行中的后代都会让该行显示蓝色活动指示器,并在悬停与无障碍文本中给出确切数量,同时不会把空闲 parent 描述为正在运行。普通 fork 会开启单独的聚合子树。待处理交互优先于 parent 的运行中状态;二者无论哪一项存在都会保持为主要状态,而后代活动则成为悬停与无障碍状态中的第二项。两者均不存在时,后代活动优先于未查看的完成提醒;最后一个运行中的后代停止后,该提醒会恢复。每个健康的直接目录行都携带读取时的 `hasChildren` 提示,该值只根据持久化 `origin: 'subagent'` 的直接谱系 header 派生;正常的健康与 diagnostic subagent 候选都会携带该标记,而普通 fork 不会。该预查不读取任何后代事件日志,展开后仍以描述符支撑的目录为权威依据。当摘要在该目录尚不存在时或在一次陈旧的空响应后确认已有后代时,该操作会保持可见,并且在打开它以刷新目录之前仅显示禁用的加载行;仅由摘要支撑的行绝不会提供导航能力。UI 会在交互前就省略已知叶子节点的展开控件;该提示不承诺 child 会一直是叶子。已展开的直接目录加载期间,已知谱系会为每个直接后代预留一行禁用的加载行,而不会递归获取后代目录。随后树会呈现可继续与 one-shot 行;one-shot 的可选 label 缺失时,回退到其会话 id。损坏、不受支持或不可用的候选仍以禁用的 diagnostic 行显示。
`running` 表示在 Host 采样边界,确切 child Agent driver 正在处理工作;`inactive` 表示该 driver 空闲或不存在。UI 不会把任一值解释为成功、失败、取消、完成状态或可恢复性。`subagent.list` 提供当前 driver 状态基线,`host/session-status` 会就地更新已知活动状态,请求内回放会阻止更早发起但尚未完成的列表响应覆盖较新的状态转换,`host/session-removed` 则会使已知行恢复为 `inactive`;重连时会读取新的基线。直接 subagent 的 `host/session-added` 帧会立即把任何已加载的 parent 行翻转为 `hasChildren: true`,并使这项正向提示不被更早发起但尚未完成的目录响应覆盖;受影响分支打开期间,成员、label、mode、diagnostic 与权威快照仍需要通过去抖动的 `subagent.list` 刷新来更新。消息投递时仍以提示词响应为权威依据。
diff --git a/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx b/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx
index 50850f0b98..3a6ace14a5 100644
--- a/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx
+++ b/packages/client/ui-subagent/src/client/SubagentCatalogAction.tsx
@@ -433,7 +433,7 @@ export function SubagentCatalogAction({
// 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 runningCountKey = descendants.runningCount === 1 ? 'count.running.one' : 'count.running.other'
// Session summaries can announce membership before the descriptor-backed catalog catches up.
// Keep that entry point visible through disabled loading rows; only catalog rows are navigable.
const summaryBackedLoading = descendants.count > 0
@@ -564,7 +564,10 @@ export function SubagentCatalogAction({
className={css.trigger}
aria-haspopup="tree"
aria-expanded={open}
- aria-label={t(descendants.runningCount > 0 ? runningCountKey : totalCountKey, { count: descendantCount })}
+ aria-label={t(
+ descendants.runningCount > 0 ? runningCountKey : totalCountKey,
+ { count: descendants.runningCount > 0 ? descendants.runningCount : descendantCount },
+ )}
onClick={() => { changeOpen(!open) }}
onKeyDown={(event) => {
if (event.key !== 'ArrowDown') return
diff --git a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx
index e0610d3379..c300e423e6 100644
--- a/packages/client/ui-subagent/tests/conversation-ui.spec.tsx
+++ b/packages/client/ui-subagent/tests/conversation-ui.spec.tsx
@@ -108,7 +108,7 @@ describe('SubagentCatalogAction', () => {
}
const view = render()
- const trigger = screen.getByRole('button', { name: '3 个子代理,正在运行' })
+ const trigger = screen.getByRole('button', { name: '1 个子代理,正在运行' })
expect(trigger.querySelector('[data-state="ongoing"]')).not.toBeNull()
view.rerender( {
})) as Record
const input = props(catalog({ entries }), {}, summaries)
render()
- fireEvent.click(screen.getByRole('button', { name: /9 个子代理/ }))
+ const trigger = screen.getByRole('button', { name: '1 个子代理,正在运行' })
+ expect(within(trigger).getByText('9 个子代理')).toBeTruthy()
+ fireEvent.click(trigger)
const runningRow = screen.getByRole('treeitem', { name: /running.*4\.6K tok · 1分10秒/ })
const runningMetrics = within(runningRow)
@@ -506,7 +508,8 @@ describe('SubagentCatalogAction', () => {
const absent = props(undefined, {}, summaries)
const view = render()
- const trigger = screen.getByRole('button', { name: '2 个子代理,正在运行' })
+ const trigger = screen.getByRole('button', { name: '1 个子代理,正在运行' })
+ expect(within(trigger).getByText('2 个子代理')).toBeTruthy()
fireEvent.click(trigger)
expect(absent.setCatalogOpen).toHaveBeenCalledWith(PARENT, true)
expect(screen.getAllByRole('treeitem', { name: '正在加载子代理' })).toHaveLength(2)
@@ -514,7 +517,7 @@ describe('SubagentCatalogAction', () => {
const staleEmpty = props(catalog({ entries: [] }), {}, summaries)
view.rerender()
- expect(screen.getByRole('button', { name: '2 个子代理,正在运行' })).toBeTruthy()
+ expect(screen.getByRole('button', { name: '1 个子代理,正在运行' })).toBeTruthy()
expect(screen.getAllByRole('treeitem', { name: '正在加载子代理' })).toHaveLength(2)
expect(staleEmpty.openChild).not.toHaveBeenCalled()
})
diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml
index c5cfc89ea5..047513c346 100644
--- a/packages/client/ui-workspace/README.i18n.yaml
+++ b/packages/client/ui-workspace/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
-README.md: b2baea049c30f46c3194009e71c70b38973dc526
-README.zh.md: cc50ba3691db10a533337b0e99689fba72a679ca
+README.md: 1ec07bd41e72bb5a26b2cfc3bf90e57e7d92db08
+README.zh.md: 18d9e356a9e5deb38624f377d41b2fc8bc677294
diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md
index b2baea049c..1ec07bd41e 100644
--- a/packages/client/ui-workspace/README.md
+++ b/packages/client/ui-workspace/README.md
@@ -16,7 +16,7 @@ Session rows render the runtime's live `pendingInteraction` classification: appr
Both target slots are declared by other plugins, so `apply` uses `slots.inject()` to register for each declaration lifetime and re-register after a declaring slot is restored.
-The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`; users enter those conversations through the selected parent's subagent header catalog. Each visible ordinary row inherits the blue activity indicator while any descendant reached through uninterrupted subagent-origin lineage is running, and its hover and assistive text report the exact running-descendant count without describing an idle parent as running. Ordinary forks remain visible and terminate this aggregation because lineage alone does not set their origin. Pending user interaction remains the primary row marker while descendant activity stays available as a separate hover and assistive status. The runtime keeps hidden rows available for conversation, title, and addressed transport state.
+The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`; users enter those conversations through the selected parent's subagent header catalog. Each visible ordinary row inherits the blue activity indicator while any descendant reached through uninterrupted subagent-origin lineage is running, and its hover and assistive text report the exact running-descendant count without describing an idle parent as running. Ordinary forks remain visible and terminate this aggregation because lineage alone does not set their origin. Pending user interaction outranks the session's own running state, and either remains the primary row status while descendant activity stays available as a separate hover and assistive status. With neither present, descendant activity outranks the green unviewed-completion reminder; the reminder returns once no descendant is running. The runtime keeps hidden rows available for conversation, title, and addressed transport state.
## Model Experience
diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md
index cc50ba3691..18d9e356a9 100644
--- a/packages/client/ui-workspace/README.zh.md
+++ b/packages/client/ui-workspace/README.zh.md
@@ -16,7 +16,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
两个目标 slot 都由其他插件声明,因此 `apply` 使用 `slots.inject()` 在各自的声明生命周期内完成注册,并在目标 slot 的声明恢复后重新注册。
-共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;用户从所选 parent 的 subagent 页头目录进入这些对话。每个可见的普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承蓝色活动指示器;其悬停与无障碍文本会报告确切的运行中后代数量,同时不会把空闲 parent 描述为正在运行。普通 fork 仍然可见,并会终止此聚合,因为仅有谱系不会设置该 origin。待处理的用户交互仍是主要行标记,而后代活动会作为独立的悬停与无障碍状态保留。运行时仍保留隐藏行,供对话、标题与已寻址传输状态使用。
+共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;用户从所选 parent 的 subagent 页头目录进入这些对话。每个可见的普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承蓝色活动指示器;其悬停与无障碍文本会报告确切的运行中后代数量,同时不会把空闲 parent 描述为正在运行。普通 fork 仍然可见,并会终止此聚合,因为仅有谱系不会设置该 origin。待处理的用户交互优先于会话自身的运行中状态,二者无论哪一项存在都会保持为行的主要状态,而后代活动仍作为独立的悬停与无障碍状态保留。两者均不存在时,后代活动优先于绿色的未查看完成提醒;最后一个运行中的后代停止后,该提醒会重新出现。运行时仍保留隐藏行,供对话、标题与已寻址传输状态使用。
## 模型体验
diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx
index 3d3b40403d..40ef1e8883 100644
--- a/packages/client/ui-workspace/src/client/rows/Rows.tsx
+++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx
@@ -176,7 +176,10 @@ interface SessionStatus {
label: string
}
-/** Session status presentation; pending user interaction remains primary. */
+/**
+ * Session status presentation; pending interaction is primary and live activity
+ * outranks completion reminders.
+ */
function sessionStatuses(
node: Pick,
t: RowTranslate,
@@ -386,9 +389,9 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
drag.drop(rowHalf(e))
}}
>
- {/* Pending interactions and running outrank the idle state; a
- finished-but-unviewed session shows the green done reminder dot
- (cleared by opening the session). */}
+ {/* Pending interaction and own or descendant activity outrank the
+ finished-but-unviewed reminder, which returns after activity stops
+ and is cleared by opening the session. */}
{(primaryStatus.state !== 'done' || row.completed) && (
<>
diff --git a/packages/client/ui-workspace/tests/rows.spec.tsx b/packages/client/ui-workspace/tests/rows.spec.tsx
index 46eb8b2dba..7e0971cf72 100644
--- a/packages/client/ui-workspace/tests/rows.spec.tsx
+++ b/packages/client/ui-workspace/tests/rows.spec.tsx
@@ -133,7 +133,7 @@ describe('workspace browser rows', () => {
expect(onOpen).toHaveBeenCalledWith(node.id)
})
- it('shows the green done dot only on a finished, unviewed session (running wins the slot)', () => {
+ it('shows the green done dot only on a finished, unviewed session (live activity wins the slot)', () => {
const renderRow = (over: Partial) => render(
{
const running = renderRow({ completed: true, running: true })
expect(running.container.querySelector('[data-state="ongoing"]')).not.toBeNull()
expect(running.container.querySelector('[data-state="done"]')).toBeNull()
+ running.unmount()
+ // Descendant activity also wins until the last running descendant stops.
+ const delegated = renderRow({ completed: true, runningSubagentCount: 1 })
+ expect(delegated.container.querySelector('[data-state="ongoing"]')).not.toBeNull()
+ expect(delegated.container.querySelector('[data-state="done"]')).toBeNull()
})
it('shows descendant activity without describing an idle parent as running', () => {
@@ -182,6 +187,29 @@ describe('workspace browser rows', () => {
}
})
+ it('keeps descendant activity secondary while the parent is running', () => {
+ vi.useFakeTimers()
+ try {
+ const node: SessionNode = {
+ id: sid('owner'), title: 'Delegating', blank: false, running: true,
+ runningSubagentCount: 1, completed: false, updatedAt: 0,
+ }
+ render()
+ const row = screen.getByRole('treeitem')
+ expect(row.querySelectorAll('[data-state="ongoing"]')).toHaveLength(1)
+ expect(screen.getByText('进行中')).toBeTruthy()
+ expect(screen.getByText('1 个子代理运行中')).toBeTruthy()
+
+ fireEvent.pointerEnter(row.parentElement as HTMLElement)
+ act(() => { vi.advanceTimersByTime(500) })
+ expect(screen.getAllByText('进行中')).toHaveLength(2)
+ expect(screen.getAllByText('1 个子代理运行中')).toHaveLength(2)
+ } finally {
+ vi.useRealTimers()
+ }
+ })
+
it('keeps child activity as a secondary status while user attention is primary', () => {
const node: SessionNode = {
id: sid('owner'), title: 'Needs input', blank: false, pendingInteraction: 'question',