fix(web): address subagent header review findings

This commit is contained in:
yx.zhang
2026-08-20 21:12:07 +08:00
parent de572dd910
commit 5f7ac9183e
14 changed files with 90 additions and 69 deletions
@@ -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: 592a0ef2bbf050cf86617facfd56adcd6377b63e
2026-07-27-web-subagent-conversations.zh.md: 07c852a9492e10bf454916462bdb474d7c66a1ec
2026-07-27-web-subagent-conversations.md: c897d345d9749facfb2046104b7a95076cf2df95
2026-07-27-web-subagent-conversations.zh.md: 0fdfd19b6dac56c275bb2d2306ed492a269e70d2
@@ -37,7 +37,7 @@ The Figma [subagent list](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5J8/Ha
## Product contract
The descendant-count control is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. On an ordinary session, a slash separates the current title from that count control. Every subagent breadcrumb combines its compact 12px title with a fixed double chevron: the current breadcrumb uses primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, with a short crossing grace for the portaled menu; ArrowDown remains the keyboard entry path. Clicking an ancestor cancels pending hover, closes an open catalog, and only navigates upward. Each direct-parent catalog enables sibling switching and bolds its selected row; its label overrides the session-summary title, and a missing ancestor catalog refreshes automatically. Only the current subagent appends its own descendant-count control. Long titles truncate before the fixed chevron. The count 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.
The descendant-count control is absent only when a complete empty direct-catalog response agrees with the session-summary projection that no subagent descendants are known. On an ordinary session, a slash separates the current title from that count control. Every subagent breadcrumb combines its compact 12px title with a fixed double chevron: the current breadcrumb uses primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, with a short crossing grace for the portaled menu; ArrowDown remains the keyboard entry path. Clicking an ancestor cancels pending hover, closes an open catalog, and only navigates upward. Each direct-parent catalog enables sibling switching and bolds its selected row; its label overrides the session-summary title, and every missing switcher catalog refreshes automatically. Only the current subagent appends its own descendant-count control. Long titles truncate before the fixed chevron. The count 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.
@@ -69,7 +69,7 @@ The adapter stays in `dsh-host-apiproxy`; `dsh-host-webserver` remains a carrier
The React-free runtime owns catalogs, single-flight refreshes, retained addresses, availability hints, transport selection, and a reference-stable map of each list row's current projection values. Re-selecting a known child retains its address so navigation cannot silently switch to ordinary session APIs. A missing intermediate breadcrumb address can be recovered from an already-loaded ancestor catalog, but it is not retained for transport and creates no scope until the user selects that breadcrumb. Restored navigation persists the full mode-bearing address.
Catalogs ride the standard `useSessions` snapshot. Component-local state owns menu visibility, expanded branches, focus, and hover timers. `ui-conversation` declares a lineage slot for every subagent breadcrumb, passing its session id, ordinary title fallback, and an upward-navigation callback for ancestors; it contains no catalog-specific condition. `@deepseek-ai/dsh-client-ui-subagent` occupies each lineage slot with direct-parent catalog navigation and elects a reason-specific read-only composer from ordinary owner props. Components receive derived props and callbacks, never `ctx`.
Catalogs ride the standard `useSessions` snapshot. Component-local state owns menu visibility, expanded branches, focus, and hover timers. `ui-conversation` declares a lineage slot for the current ordinary title and every subagent breadcrumb, passes plain breadcrumb identity and display text plus an upward-navigation callback for ancestors, and retains the ordinary title as the render fallback. `@deepseek-ai/dsh-client-ui-subagent` occupies each lineage slot with direct-parent catalog navigation and elects a reason-specific read-only composer from ordinary owner props. Components receive derived props and callbacks, never `ctx`.
Every in-process subagent child stamps `SessionHeader.origin: 'subagent'` before publication. Session list summaries and incremental Host frames project it so grouped and flat sidebars omit duplicate child rows while preserving ordinary forks. The same existing `host/session-added` frame marks a loaded direct parent row expandable without introducing a catalog event stream. Descriptor mode and catalog verification remain the authority for navigation, continuation, and authorization.
@@ -37,7 +37,7 @@ Figma 中的 [subagent 列表](https://www.figma.com/design/jRBBK7zBgcszdVWQ0Fh5
## 产品约定
只有当完整的直接目录空响应与会话摘要投影相符,二者均表明没有已知的 subagent 后代时,才不显示后代数量控件。普通会话会用斜杠分隔当前 title 和该数量控件。每一级 subagent 面包屑都将紧凑的 12px title 与固定显示的双向箭头组合成一个控件:当前面包屑使用主标签颜色和 500 字重,祖先则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录;浮层菜单具有短暂的跨越宽限时间,ArrowDown 保留为键盘入口。点击祖先会取消待触发的悬停、关闭已打开的目录,并且只向上导航。每个直接 parent 目录都可切换 sibling,并会加粗其选中行;目录 label 优先于会话摘要 title,缺失的祖先目录会自动刷新。只有当前 subagent 会追加自己的后代数量控件。过长的 title 会在固定箭头之前截断。数量触发器会统计经不间断的 `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 后代时,才不显示后代数量控件。普通会话会用斜杠分隔当前 title 和该数量控件。每一级 subagent 面包屑都将紧凑的 12px title 与固定显示的双向箭头组合成一个控件:当前面包屑使用主标签颜色和 500 字重,祖先则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录;浮层菜单具有短暂的跨越宽限时间,ArrowDown 保留为键盘入口。点击祖先会取消待触发的悬停、关闭已打开的目录,并且只向上导航。每个直接 parent 目录都可切换 sibling,并会加粗其选中行;目录 label 优先于会话摘要 title,缺失的每个切换器目录会自动刷新。只有当前 subagent 会追加自己的后代数量控件。过长的 title 会在固定箭头之前截断。数量触发器会统计经不间断的 `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` 刷新来更新。消息投递时仍以提示词响应为权威依据。
@@ -69,7 +69,7 @@ one-shot 行始终会用文案替代输入框,说明执行记录为只读。
不依赖 React 的运行时负责目录、单次并发刷新、保留的地址、可用性提示、传输选择,以及每个列表行当前投影值的引用稳定映射。再次选择已知 child 时会保留其地址,避免导航静默切换到普通会话 API。缺失的中间面包屑地址可以从已加载的祖先目录恢复,但在用户选择该面包屑之前不会保留为传输地址,也不会创建 scope。恢复的导航会持久化包含 mode 的完整地址。
目录通过标准 `useSessions` 快照传递。组件局部状态负责菜单可见性、已展开分支、焦点与悬停计时器。`ui-conversation` 为每一级 subagent 面包屑声明谱系 slot,传入该级会话 id、普通 title 回退,并为祖先传入向上导航回调;其中没有目录专用条件`@deepseek-ai/dsh-client-ui-subagent` 以直接 parent 目录导航占用每个谱系 slot,并根据普通 owner props 选择按原因区分的只读编辑器。组件只接收派生 props 与回调,绝不接收 `ctx`
目录通过标准 `useSessions` 快照传递。组件局部状态负责菜单可见性、已展开分支、焦点与悬停计时器。`ui-conversation`当前普通 title 和每一级 subagent 面包屑声明谱系 slot,传入纯数据形式的面包屑身份与显示文本,并为祖先传入向上导航回调;普通 title 由 render site 保留为回退`@deepseek-ai/dsh-client-ui-subagent` 以直接 parent 目录导航占用每个谱系 slot,并根据普通 owner props 选择按原因区分的只读编辑器。组件只接收派生 props 与回调,绝不接收 `ctx`
每个进程内 subagent child 都会在发布前写入 `SessionHeader.origin: 'subagent'`。会话列表摘要与增量 Host 帧会投影该字段,使分组和扁平侧边栏省略重复的 child 行,同时保留普通 fork。同一条现有的 `host/session-added` 帧还会把已加载的直接 parent 行标记为可展开,而无需引入目录事件流。描述符 mode 与目录校验仍然是导航、继续执行和授权的权威依据。
@@ -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-conversation/README.md
README.md: 2e295ddfdbfffc83b08af5cababda83d1672e6e2
README.zh.md: 76395003e4858fa41c3329301039aed31633ecad
README.md: 266cc3cb2308815827031b8e294a06b64fb958dc
README.zh.md: 9bfa227df014ea68c87f4761a0aeb8731a7924f5
+1 -1
View File
@@ -16,7 +16,7 @@ Chat business rows are independent registry contributions rather than a closed b
Approvals take over the composer through the chain this package declares: `ApprovalPanel` registers as a selector-routed `'conversation.composer'` entry (the ui-user-questions pattern) and occupies the composer in place of the InputBar while an approval wait is pending (amber strip, justification headline, paired command line from the running call's args, one-shot refuse/allow). The `PendingApproval` domain face in `contract/slots.ts` owns the wire encoding — the `ApprovalResponsePayload` value with the audit correlation — over the runtime's `PendingWait` carrier; the broadcast `approval/resolved` frame settles the wait and restores the composer. The runtime manager projects every approval or question wait through `SessionSummary.pendingInteraction`, including sessions never instantiated; `ui-workspace` owns its sidebar presentation. Pending waits leave the message flow entirely: questions (ui-user-questions) and approvals (ApprovalPanel) both answer through the composer takeover, so no display-only placeholder card remains. The composer's bottom-row Access seat mounts `PermissionSelect`, fed by the host-computed `permissions` projection through the standard-kit `useProjection` (key absence hides the chip); the chip opens a Menu-primitive dropdown whose built-in preset ids render as localized product labels only when the host keeps their canonical names, while explicit host labels remain unchanged and unknown kebab-case names render in title case. Safe preset picks submit `/permission <preset>` immediately through the bar's injected `command` callback, while `danger-full-access` first opens an in-page Modal risk confirmation. The enabling action stays disabled until the user checks the acknowledgement; cancel, Escape, close, and mask click submit nothing.
The session header renders each subagent breadcrumb title through the optional session-scoped `'conversation.session.header.lineage'` seat, followed by the `'conversation.session.header.actions'` list and the independent `'conversation.session.header.utilities'` list at the right edge. Each lineage owner identifies its breadcrumb session and supplies the ordinary title as fallback; an ancestor also supplies its upward-navigation callback. Removing the occupant restores every title without affecting header actions, and optional Session utilities cannot reorder or move either group. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership.
The session header dispatches each current ordinary title and subagent breadcrumb through the optional session-scoped `'conversation.session.header.lineage'` seat, followed by the `'conversation.session.header.actions'` list and the independent `'conversation.session.header.utilities'` list at the right edge. Each lineage owner supplies plain breadcrumb identity and display text; the render site retains the ordinary title as fallback, and an ancestor also supplies its upward-navigation callback. Removing the occupant restores every title without affecting header actions, and optional Session utilities cannot reorder or move either group. The composer chain currency includes the current conversation `session`; ui-subagent selects one-shot or parent-unavailable addressed sessions for reason-specific read-only copy, while the ordinary InputBar keeps every addressed child Send-only because the continuation service exposes no public per-Activation cancellation operation and `session.cancel` would bypass its ownership.
Logged non-user messages render as a default-collapsed disclosure whose header names the role the runtime projected for the message — `上下文注入` for an injection, `跨会话召回` for a recalled session — followed by the producer name that projection read out of the durable source, so a reader distinguishes a skill catalog from a workspace instruction file or a recalled session without expanding. A direct message that cites another session precedes its recall row in durable order. The Chat snapshot associates exact labels only from that immediately following sourced recall, preserving multi-word titles without carrying one recall's labels onto a later direct message. Recall uses a chat-bubble glyph while other context keeps the document glyph; a source that names no producer shows the role alone. Composer and user-bubble references use the same inline language: a chat-bubble, file, or folder glyph plus business-color text, without a nested capsule. Like claimed slash commands, composer references keep their complete display text in the transparent textarea and use the aligned backdrop for color and the leading domain glyph; native text metrics own width, wrapping, selection, and caret placement. The occurrence range remains structured for serialization and boundary deletion, while an edit inside it converts the remaining characters to ordinary text. The session draft mirror stores each occurrence's clipboard projection, so a remount without the occurrence table restores canonical parseable reference text instead of a display-only label. The shared `DisclosureRow` primitive gives this context surface the same compact geometry as other flow rows while retaining context semantics: the expanded body follows its content height up to a 141px scrolling cap and synthesizes no tool state or summary ([historical disclosure decision](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md), [producer-label decision](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md)). That body follows the form the producer declared on its durable source: `instructions` names the reconciled files above their text, `catalog` lists the entries the source recorded instead of the model-facing prose, and every other value — absent, unknown to this version, or carrying no usable fields — renders the opaque body, which shows the model-facing text with its real line breaks and the remaining source fields. The opaque body is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here. A durable or pending steering bubble shares the user bubble's presentation unadorned; its mid-turn position in the flow is the only steering signal the transcript shows.
+1 -1
View File
@@ -14,7 +14,7 @@
Chat 业务行是彼此独立的注册表贡献,不是封闭的内建联合。Client 插件通过 declaration merging 增加类型化 `ChatNodeDataMap` key,在 `ctx.conversationEvents` 上注册 `ConversationNodeDefinition`,再向 `conversation.chat.node` 注册匹配的 keyed renderer;它无须修改会话 fold 或中央 renderer switch。稳定事件 id、append/prepend 回放、Location data 与 renderer 约束见 [Conversation Node 实操手册](../../../docs/cookbook/adding-a-conversation-node.zh.md)。
会话页头通过可选的会话作用域 `'conversation.session.header.lineage'` seat 渲染每一级 subagent 面包屑 title,随后依次渲染 `'conversation.session.header.actions'` 列表和最右侧独立的 `'conversation.session.header.utilities'` 列表。每个谱系 owner 都会标识其面包屑会话,并提供普通 title 作为回退祖先还会提供向上导航的回调。移除 occupant 会恢复每个 title,且不影响页头操作;可选的会话工具不会改变这两个区域的顺序或位置。编辑器链的 currency 包含当前对话 `session`ui-subagent 会选取 one-shot 或 parent 不可用的已寻址会话,并按原因显示只读文案,而普通 InputBar 会让所有已寻址 child 仅保留 Send,因为继续执行服务不公开逐 Activation 取消操作,`session.cancel` 也会绕过其所有权。
会话页头通过可选的会话作用域 `'conversation.session.header.lineage'` seat 派发当前普通 title 与每一级 subagent 面包屑,随后依次渲染 `'conversation.session.header.actions'` 列表和最右侧独立的 `'conversation.session.header.utilities'` 列表。每个谱系 owner 都会提供纯数据形式的面包屑身份与显示文本;render site 保留普通 title 作为回退祖先还会提供向上导航的回调。移除 occupant 会恢复每个 title,且不影响页头操作;可选的会话工具不会改变这两个区域的顺序或位置。编辑器链的 currency 包含当前对话 `session`ui-subagent 会选取 one-shot 或 parent 不可用的已寻址会话,并按原因显示只读文案,而普通 InputBar 会让所有已寻址 child 仅保留 Send,因为继续执行服务不公开逐 Activation 取消操作,`session.cancel` 也会绕过其所有权。
已记录的非用户消息渲染为默认折叠的展开项,标题栏先给出运行时为该消息投影出的角色——注入为 `上下文注入`,召回为 `跨会话召回`——其后是该投影从持久来源读出的生产者名称,因此读者无需展开即可区分 skill(技能)目录、工作区指令文件与被召回的会话。引用其他会话的直接消息在持久顺序中位于其召回行之前。Chat 快照只从紧随其后的带来源召回中关联准确标签,因此既能保留多词标题,也不会把一条召回的标签带到后续直接消息上。召回使用聊天气泡图标,其他上下文保留文档图标;来源未提供生产者名称时只显示角色。输入框与用户气泡中的引用使用同一种行内语言:聊天气泡、文件或文件夹图标加业务色文字,不嵌套胶囊容器。与已认领的 slash command 相同,输入框引用会把完整展示文本保留在透明 textarea 中,再用对齐的 backdrop 提供颜色和开头的领域图标;宽度、换行、选择区与光标位置均由原生文本度量决定。occurrence 范围仍为序列化与边界整段删除保留结构身份,在范围内部编辑则会把剩余字符转为普通文本。会话草稿镜像会存储每个 occurrence 的剪贴板投影,因此在 occurrence 表缺失的情况下重新挂载时,会恢复可解析的规范引用文本,而不是仅供显示的标签。共享的 `DisclosureRow` 原子组件让该上下文界面与消息流中的其他紧凑行保持相同几何,同时保留上下文语义:展开内容区的高度会随内容自适应,最大为 141px,超出后滚动,且不会合成工具状态或摘要([历史展开项决策](../../../.agents/notes/archived/feature/2026-07-30-web-context-injection-disclosure.md)、[生产者标签决策](../../../.agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.zh.md))。该内容区按生产方在持久来源上声明的形态渲染:`instructions` 在正文之上列出它对账过的文件,`catalog` 列出来源记录的条目而非面向模型的正文,其余取值——未声明、本版本不认识、或字段不可用——一律渲染 opaque 内容区,即按真实换行展示面向模型的文本,并把剩余来源字段列出。opaque 不是兜底剩余物而是有文档的默认:恢复的、fork 的、外部写入的日志,无论其生产方是否挂载在此处,都必须渲染得出来。持久或待处理的 steering(中途引导)气泡沿用用户气泡的呈现,不加任何装饰;transcript 中唯一的 steering 信号是它出现在轮次中途的位置。
@@ -78,9 +78,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
*/
'conversation.session.header': { kind: 'single'; scope: 'session' }
/**
* One breadcrumb title and its lineage controls. The owner supplies the
* ordinary title as both slot currency and render fallback; an occupant
* may replace it with one combined navigation control.
* One breadcrumb title and its lineage controls. The render site keeps
* the ordinary title as fallback; an occupant receives plain title data
* and may replace a subagent title with one combined navigation control.
*/
'conversation.session.header.lineage': {
kind: 'single'
@@ -315,12 +315,10 @@ export interface ConversationSessionOwnerProps {
/** Header actions derive their state from the standard session/global kit. */
export interface ConversationHeaderActionOwnerProps {}
/** Breadcrumb-title content handed to the optional lineage renderer. */
/** Plain breadcrumb data handed to the optional lineage renderer. */
export interface ConversationHeaderLineageOwnerProps {
/** Session represented by this breadcrumb title. */
lineageSessionId: SessionId
/** Ordinary breadcrumb-title node retained when no lineage renderer is installed. */
title: ReactNode
/** Display title available to a renderer that combines the title with a control. */
displayTitle: string
/** Navigate to an ancestor title when its combined control is clicked. */
@@ -103,16 +103,31 @@ export function ConversationSessionHeader({
</button>
)
const lineage = last || summary.subagent
const lineageOwner = {
lineageSessionId: summary.id,
displayTitle: summary.displayTitle,
...last ? {} : { openTitle: () => { open(summary.id) } },
}
return (
<span key={summary.id} className={css.crumbSeg}>
{index > 0 && <span className={css.crumbSep}>/</span>}
{lineage
? renderSlot('conversation.session.header.lineage', {
lineageSessionId: summary.id,
title,
displayTitle: summary.displayTitle,
...last ? {} : { openTitle: () => { open(summary.id) } },
}, { fallback: title })
? summary.subagent
? renderSlot(
'conversation.session.header.lineage',
lineageOwner,
{ fallback: title },
)
: (
<>
{title}
{renderSlot(
'conversation.session.header.lineage',
lineageOwner,
{ fallback: null },
)}
</>
)
: title}
</span>
)
+2 -2
View File
@@ -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-subagent/README.md
README.md: 50ed35d5229ffe6f65c81b6b641758ead877a86e
README.zh.md: 54d83e81d09e4a979663c77c87cda72f7f30b1db
README.md: d4a804a94b0f2cb295fd29c09c7f32385e423495
README.zh.md: 3cec2109cbb5f85617cf69155c5582ee43496f09
+1 -1
View File
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Web subagent feature owner: contributes current-title lineage navigation to `conversation.session.header.lineage`, reason-specific read-only replacements to the conversation composer chain, and the existing `@` reference source to `ctx.inputTriggers`.
The header lineage renderer reads `subagentsByParent` and session summaries through the standard `useSessions` hook. On an ordinary session it preserves the current title and, when descendants exist, appends `/` plus the descendant-count dropdown before the header action row. Every subagent breadcrumb uses the count trigger's 12px type scale and combines its title with a fixed double chevron; the current breadcrumb keeps primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, ArrowDown remains the keyboard entry path, and clicking an ancestor cancels a pending menu and navigates upward without opening it. Each catalog enables sibling switching and bolds its selected row; a catalog label overrides the session-summary title, and an ancestor whose catalog has not loaded requests it automatically. Only the current subagent appends its own descendant-count dropdown. Long breadcrumb titles truncate while the chevron remains visible. The count trigger covers the complete subagent-only descendant lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. The compact tree remains direct-catalog authoritative: continuable and one-shot rows display mode plus `running`/`inactive` activity and an optional log-backed title, while the trailing column stacks total durable provider usage above active-turn duration. Token totals sum the four disjoint `tokenUsage` buckets. Visual duration stays exact to the second below one day, then uses at most two adjacent units—days/hours, approximate months/days, or approximate years/months—while hover and the accessible name retain the exact day/hour/minute/second value. Duration sums completed `subagentTiming` turns, advances once per second only for an open turn on a running child, and freezes after the child becomes inactive; an interrupted open turn is bounded by its same-cut `active.through`, never by newer session metadata. An unlabeled one-shot row falls back to its session id, while corrupt, unsupported, or unavailable rows remain readable but disabled. Each healthy row's `hasChildren` hint determines disclosure before interaction, so known leaves never show an arrow; a catalog level reserves the disclosure column only when at least one healthy row is a branch, allowing branchless levels to start at the leading status marker. Expanding a branch immediately reserves one disabled loading row per known direct descendant, then lazily replaces them with that child's authoritative catalog. Every visible branch is reported to the runtime so membership frames cause a debounced refresh only where the tree is being consumed. Selecting any depth calls `SessionRuntime.openSubagent()` with the row's exact `{parentSessionId, childSessionId, mode}` address. Component-local state owns tree visibility, expanded branches, keyboard focus, and the running-duration clock. ArrowRight/ArrowLeft expand and collapse branches; ArrowUp/ArrowDown, Home, End, and Escape navigate or close the tree; closing returns focus to the trigger. Styling uses tokens only.
The header lineage renderer reads `subagentsByParent` and session summaries through the standard `useSessions` hook. On an ordinary session it preserves the current title and, when descendants exist, appends `/` plus the descendant-count dropdown before the header action row. Every subagent breadcrumb uses the count trigger's 12px type scale and combines its title with a fixed double chevron; the current breadcrumb keeps primary color and weight 500, while ancestors use tertiary color and weight 400. Hovering a combined control for 150ms opens its direct-parent catalog, ArrowDown remains the keyboard entry path, and clicking an ancestor cancels a pending menu and navigates upward without opening it. Each catalog enables sibling switching and bolds its selected row; a catalog label overrides the session-summary title, and every switcher whose catalog has not loaded requests it automatically. Only the current subagent appends its own descendant-count dropdown. Long breadcrumb titles truncate while the chevron remains visible. The count trigger covers the complete subagent-only descendant lineage, stops at ordinary forks, and shows ongoing activity when any counted descendant is running. The compact tree remains direct-catalog authoritative: continuable and one-shot rows display mode plus `running`/`inactive` activity and an optional log-backed title, while the trailing column stacks total durable provider usage above active-turn duration. Token totals sum the four disjoint `tokenUsage` buckets. Visual duration stays exact to the second below one day, then uses at most two adjacent units—days/hours, approximate months/days, or approximate years/months—while hover and the accessible name retain the exact day/hour/minute/second value. Duration sums completed `subagentTiming` turns, advances once per second only for an open turn on a running child, and freezes after the child becomes inactive; an interrupted open turn is bounded by its same-cut `active.through`, never by newer session metadata. An unlabeled one-shot row falls back to its session id, while corrupt, unsupported, or unavailable rows remain readable but disabled. Each healthy row's `hasChildren` hint determines disclosure before interaction, so known leaves never show an arrow; a catalog level reserves the disclosure column only when at least one healthy row is a branch, allowing branchless levels to start at the leading status marker. Expanding a branch immediately reserves one disabled loading row per known direct descendant, then lazily replaces them with that child's authoritative catalog. Every visible branch is reported to the runtime so membership frames cause a debounced refresh only where the tree is being consumed. Selecting any depth calls `SessionRuntime.openSubagent()` with the row's exact `{parentSessionId, childSessionId, mode}` address. Component-local state owns tree visibility, expanded branches, keyboard focus, and the running-duration clock. ArrowRight/ArrowLeft expand and collapse branches; ArrowUp/ArrowDown, Home, End, and Escape navigate or close the tree; closing returns focus to the trigger. Styling uses tokens only.
A one-shot child always elects a read-only composer that identifies the transcript as a completed execution record. A continuable child does so only when its exact parent is unavailable and the child is not running, with copy explaining the recovery path; while such a child still runs, the selector yields to the ordinary composer, whose input and Send action are disabled but whose independent Stop stays usable, and the takeover returns once it stops. A continuable child with a live parent keeps the ordinary input chrome, whose Session routes prompts through `subagent.prompt`: typing and Send stay available while the child runs because every follow-up joins the child's FIFO inbox, while an independent Stop routes through `subagent.interrupt`. This package never receives host context or calls a model-facing tool. The catalog and composer behavior are specified by the [Web subagent conversations Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.md) and the [current-turn interrupt Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.md).
+1 -1
View File
@@ -4,7 +4,7 @@
Web subagent 功能 owner:向 `conversation.session.header.lineage` 贡献当前 title 谱系导航,向会话编辑器链贡献按原因区分的只读替代呈现,并保留注册到 `ctx.inputTriggers` 的既有 `@` 引用 source。
页头谱系 renderer 通过标准 `useSessions` 钩子读取 `subagentsByParent` 与会话摘要。普通会话保留当前 title,并在存在后代时,于页头操作行之前追加 `/` 和后代数量下拉菜单。每一级 subagent 面包屑都使用数量触发器的 12px 字号,并将 title 与固定显示的双向箭头组合成一个控件;当前面包屑保留主标签颜色和 500 字重,祖先面包屑则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录,ArrowDown 保留为键盘入口;点击祖先面包屑会取消待打开的菜单并仅向上导航。每个目录都可切换 sibling,并会加粗选中行;目录 label 优先于会话摘要 title,尚未加载目录的祖先会自动请求该目录。只有当前 subagent 会追加自己的后代数量下拉菜单。过长的面包屑 title 会截断,而双向箭头始终保留。数量触发器会统计仅含 subagent 的完整后代谱系,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。紧凑树仍以直接目录为权威依据:可继续和 one-shot 行会显示 mode、`running``inactive` 活动状态和由日志支撑的可选 title,尾随列则在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。token 用量总计为四个互不重叠的 `tokenUsage` 桶之和。视觉耗时在不足一天时精确到秒,达到一天后则最多使用两个相邻单位——天/小时、近似月份/天或近似年份/月份——而悬停信息与无障碍名称会保留精确的天/小时/分钟/秒数值。耗时会累加已完成的 `subagentTiming` 轮次,仅在运行中 child 存在未结束轮次时每秒递增一次,并在 child 变为 inactive 后冻结;被中断的未结束轮次以其同一切面的 `active.through` 为上界,绝不使用更新的会话元数据。没有 label 的 one-shot 行会回退到其会话 id,而损坏、不受支持或不可用的行仍保持可读但禁用。每个健康行的 `hasChildren` 提示会在交互前决定是否显示展开控件,因此已知叶子节点从不显示箭头;每层目录仅在其中至少一个健康行是分支时才预留展开列,使完全不含分支的层级能从最前面的状态标记开始。展开分支时,会立即为每个已知直接后代预留一行禁用的加载行,随后再用该 child 的权威目录懒加载结果替换这些占位行。每个可见分支都会上报给运行时,使成员帧只在树正被消费的位置触发去抖动刷新。选择任意深度的条目都会使用该行的确切地址 `{parentSessionId, childSessionId, mode}` 调用 `SessionRuntime.openSubagent()`。组件局部状态负责树的可见性、已展开分支、键盘焦点与运行中耗时时钟。ArrowRightArrowLeft 展开和折叠分支;ArrowUpArrowDown、Home、End 与 Escape 用于导航或关闭树;关闭后焦点返回触发器。样式只使用 token。
页头谱系 renderer 通过标准 `useSessions` 钩子读取 `subagentsByParent` 与会话摘要。普通会话保留当前 title,并在存在后代时,于页头操作行之前追加 `/` 和后代数量下拉菜单。每一级 subagent 面包屑都使用数量触发器的 12px 字号,并将 title 与固定显示的双向箭头组合成一个控件;当前面包屑保留主标签颜色和 500 字重,祖先面包屑则使用三级标签颜色和 400 字重。悬停组合控件 150ms 后会打开其直接 parent 目录,ArrowDown 保留为键盘入口;点击祖先面包屑会取消待打开的菜单并仅向上导航。每个目录都可切换 sibling,并会加粗选中行;目录 label 优先于会话摘要 title,尚未加载目录的每个切换器都会自动请求该目录。只有当前 subagent 会追加自己的后代数量下拉菜单。过长的面包屑 title 会截断,而双向箭头始终保留。数量触发器会统计仅含 subagent 的完整后代谱系,在普通 fork 处停止,并在任一计入统计的后代处于 `running` 时显示活动仍在进行。紧凑树仍以直接目录为权威依据:可继续和 one-shot 行会显示 mode、`running``inactive` 活动状态和由日志支撑的可选 title,尾随列则在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。token 用量总计为四个互不重叠的 `tokenUsage` 桶之和。视觉耗时在不足一天时精确到秒,达到一天后则最多使用两个相邻单位——天/小时、近似月份/天或近似年份/月份——而悬停信息与无障碍名称会保留精确的天/小时/分钟/秒数值。耗时会累加已完成的 `subagentTiming` 轮次,仅在运行中 child 存在未结束轮次时每秒递增一次,并在 child 变为 inactive 后冻结;被中断的未结束轮次以其同一切面的 `active.through` 为上界,绝不使用更新的会话元数据。没有 label 的 one-shot 行会回退到其会话 id,而损坏、不受支持或不可用的行仍保持可读但禁用。每个健康行的 `hasChildren` 提示会在交互前决定是否显示展开控件,因此已知叶子节点从不显示箭头;每层目录仅在其中至少一个健康行是分支时才预留展开列,使完全不含分支的层级能从最前面的状态标记开始。展开分支时,会立即为每个已知直接后代预留一行禁用的加载行,随后再用该 child 的权威目录懒加载结果替换这些占位行。每个可见分支都会上报给运行时,使成员帧只在树正被消费的位置触发去抖动刷新。选择任意深度的条目都会使用该行的确切地址 `{parentSessionId, childSessionId, mode}` 调用 `SessionRuntime.openSubagent()`。组件局部状态负责树的可见性、已展开分支、键盘焦点与运行中耗时时钟。ArrowRightArrowLeft 展开和折叠分支;ArrowUpArrowDown、Home、End 与 Escape 用于导航或关闭树;关闭后焦点返回触发器。样式只使用 token。
one-shot child 始终选用只读编辑器,并将 transcript(文本记录)说明为已完成的执行记录。可继续 child 仅在其确切 parent 不可用且 child 未在运行时选用只读编辑器,并以文案说明恢复路径;此类 child 仍在运行期间,selector 会让位给普通编辑器——其输入区与 Send 操作被禁用,但独立的 Stop 保持可用,停止后只读替代恢复。确切 parent 存活时,可继续 child 保留普通输入 chrome,其会话通过 `subagent.prompt` 路由提示词:child 运行期间输入和 Send 保持可用,因为每条后续消息都会进入 child 的 FIFO inbox,而独立的 Stop 经由 `subagent.interrupt` 路由。本包绝不接收宿主上下文,也不调用面向模型的工具。目录与编辑器行为由 [Web subagent 对话 Agent Note](../../../.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md) 与[当前轮次中断 Agent Note](../../../.agents/notes/implemented/feature/2026-08-06-continuable-subagent-interrupt.zh.md) 规定。
@@ -530,13 +530,13 @@ function CatalogDropdown({
useEffect(() => {
if (
!ancestorSwitcher
variant !== 'switcher'
|| catalog !== undefined
|| requestedInitialCatalog.current === rootSessionId
) return
requestedInitialCatalog.current = rootSessionId
refresh(rootSessionId)
}, [ancestorSwitcher, catalog, refresh, rootSessionId])
}, [catalog, refresh, rootSessionId, variant])
const observeCatalog = (parentSessionId: SessionId, next: boolean): void => {
if (next) observedCatalogs.current.add(parentSessionId)
@@ -567,13 +567,17 @@ function CatalogDropdown({
const changeOpen = (next: boolean, restoreFocus = false): void => {
cancelHoverOpen()
cancelHoverClose()
setOpen(next)
if (next) {
setMenuPosition(catalogMenuPosition(triggerRef.current as HTMLButtonElement))
const trigger = triggerRef.current
/* v8 ignore next -- a queued callback can outlive the trigger */
if (trigger === null) return
setOpen(true)
setMenuPosition(catalogMenuPosition(trigger))
setNow(Date.now())
observeCatalog(rootSessionId, true)
}
else {
setOpen(false)
setMenuPosition(undefined)
closeAllCatalogs()
}
@@ -641,7 +645,10 @@ function CatalogDropdown({
useEffect(() => {
if (!open) return
const placeMenu = (): void => {
setMenuPosition(catalogMenuPosition(triggerRef.current as HTMLButtonElement))
const trigger = triggerRef.current
/* v8 ignore next -- native resize or scroll can outlive the trigger */
if (trigger === null) return
setMenuPosition(catalogMenuPosition(trigger))
}
window.addEventListener('resize', placeMenu)
document.addEventListener('scroll', placeMenu, true)
@@ -676,7 +683,10 @@ function CatalogDropdown({
|| presentedCatalog.entries.length > 0
|| descendantCount > 0)
useEffect(() => {
if (visible || !open) return
if (visible) return
cancelHoverOpen()
cancelHoverClose()
if (!open) return
setOpen(false)
closeAllCatalogs()
}, [visible, open])
@@ -797,10 +807,10 @@ function CatalogDropdown({
/**
* Render one breadcrumb title together with its subagent navigation.
* @param props - Breadcrumb title, session standard props, and catalog actions.
* @returns The ordinary title plus descendant count, or a title-and-chevron sibling switcher.
* @returns An ordinary-title descendant count, or a title-and-chevron sibling switcher.
*/
export function SubagentHeaderLineage({
lineageSessionId, title, displayTitle, openTitle,
lineageSessionId, displayTitle, openTitle,
useSessions, openChild, refresh, setCatalogOpen, t,
}: SubagentHeaderLineageProps) {
const parentId = useSessions((state) => {
@@ -810,21 +820,19 @@ export function SubagentHeaderLineage({
const shared = { useSessions, openChild, refresh, setCatalogOpen, t }
if (parentId === undefined) {
return (
<>
{title}
<CatalogDropdown
key={lineageSessionId}
rootSessionId={lineageSessionId}
variant="count"
separator
{...shared}
/>
</>
<CatalogDropdown
key={lineageSessionId}
rootSessionId={lineageSessionId}
variant="count"
separator
{...shared}
/>
)
}
return (
<>
<CatalogDropdown
key={lineageSessionId}
rootSessionId={parentId}
currentSessionId={lineageSessionId}
variant="switcher"
@@ -74,7 +74,6 @@ function props(
refresh: vi.fn(),
setCatalogOpen: vi.fn(),
lineageSessionId: PARENT,
title: null,
displayTitle: 'Parent title',
t,
} as unknown as SubagentHeaderLineageProps
@@ -122,7 +121,6 @@ describe('SubagentHeaderLineage', () => {
const trigger = screen.getByRole('button', { name: '1 个子代理,正在运行' })
expect(trigger.querySelector('[data-state="ongoing"]')).not.toBeNull()
expect(trigger.children).toHaveLength(3)
view.rerender(<SubagentHeaderLineage {...props(catalog(), {}, {
...summaries,
@@ -130,7 +128,6 @@ describe('SubagentHeaderLineage', () => {
})} />)
const inactiveTrigger = screen.getByRole('button', { name: '3 个子代理' })
expect(inactiveTrigger.querySelector('[data-state="ongoing"]')).toBeNull()
expect(inactiveTrigger.children).toHaveLength(2)
})
it('does not aggregate subagents reached through an ordinary fork', () => {
@@ -260,6 +257,21 @@ describe('SubagentHeaderLineage', () => {
await vi.advanceTimersByTimeAsync(120)
})
it('cancels a pending hover when the trigger becomes hidden', async () => {
vi.useFakeTimers()
const view = render(<SubagentHeaderLineage {...props(catalog())} />)
const trigger = screen.getByRole('button', { name: /2 个子代理/ })
fireEvent.mouseEnter(trigger.parentElement!)
await vi.advanceTimersByTimeAsync(149)
view.rerender(<SubagentHeaderLineage {...props(catalog({ entries: [] }))} />)
expect(screen.queryByRole('button')).toBeNull()
await vi.advanceTimersByTimeAsync(1)
fireEvent.resize(window)
expect(screen.queryByRole('tree')).toBeNull()
})
it('covers diagnostic variants, fallback labels, and keyboard row activation', () => {
const unsupported = 'unsupported' as SessionId
const unavailable = 'unavailable' as SessionId
@@ -618,23 +630,14 @@ describe('SubagentHeaderLineage', () => {
expect(empty.setCatalogOpen).toHaveBeenCalledWith(CHILD, false)
})
it('places a separator between an ordinary title and a visible descendant count', () => {
const input = {
...props(catalog()),
title: <button type="button" disabled>Parent title</button>,
}
const view = render(<SubagentHeaderLineage {...input} />)
it('places a separator before a visible ordinary-session descendant count', () => {
const view = render(<SubagentHeaderLineage {...props(catalog())} />)
expect(screen.getByRole('button', { name: 'Parent title' })).toBeTruthy()
expect(screen.getByText('/')).toBeTruthy()
expect(screen.getByRole('button', { name: /2 个子代理/ })).toBeTruthy()
view.rerender(<SubagentHeaderLineage
{...props(catalog({ entries: [] }))}
title={<button type="button" disabled>Parent title</button>}
/>)
view.rerender(<SubagentHeaderLineage {...props(catalog({ entries: [] }))} />)
expect(screen.queryByText('/')).toBeNull()
expect(screen.getByRole('button', { name: 'Parent title' })).toBeTruthy()
})
it('combines the current subagent title and chevron into the parent-catalog trigger', () => {
@@ -651,7 +654,6 @@ describe('SubagentHeaderLineage', () => {
sessionId: CHILD,
lineageSessionId: CHILD,
displayTitle: 'worker',
title: <button type="button" disabled>fallback title</button>,
}
render(<SubagentHeaderLineage {...input} />)
@@ -659,7 +661,6 @@ describe('SubagentHeaderLineage', () => {
expect(switcher.className).toContain('switcherTrigger')
expect(within(switcher).getByText('worker')).toBeTruthy()
expect(switcher.querySelector('svg')).not.toBeNull()
expect(screen.queryByRole('button', { name: 'fallback title' })).toBeNull()
const switcherIcon = switcher.querySelector('svg')
expect(switcherIcon?.getAttribute('width')).toBe('16')
@@ -689,7 +690,6 @@ describe('SubagentHeaderLineage', () => {
sessionId: CHILD,
lineageSessionId: CHILD,
displayTitle: 'summary title',
title: <button type="button" disabled>fallback title</button>,
}
render(<SubagentHeaderLineage {...input} />)
@@ -714,7 +714,6 @@ describe('SubagentHeaderLineage', () => {
lineageSessionId: CHILD,
displayTitle: '正在扫描项目文件',
openTitle: vi.fn(),
title: <button type="button">worker</button>,
}
render(<SubagentHeaderLineage {...input} />)
@@ -735,7 +734,10 @@ describe('SubagentHeaderLineage', () => {
expect(screen.queryByRole('tree')).toBeNull()
})
it('refreshes an absent ancestor catalog without waiting for hover', () => {
it.each([
['ancestor', vi.fn()],
['current', undefined],
] as const)('refreshes an absent %s switcher catalog without waiting for hover', (_kind, openTitle) => {
const input = {
...props(undefined, {}, {
[CHILD]: {
@@ -745,8 +747,7 @@ describe('SubagentHeaderLineage', () => {
}),
lineageSessionId: CHILD,
displayTitle: '正在扫描项目文件',
openTitle: vi.fn(),
title: <button type="button">worker</button>,
...(openTitle === undefined ? {} : { openTitle }),
}
render(<SubagentHeaderLineage {...input} />)
@@ -771,7 +772,6 @@ describe('SubagentHeaderLineage', () => {
sessionId: GRANDCHILD,
lineageSessionId: GRANDCHILD,
displayTitle: 'indexer',
title: <button type="button" disabled>fallback title</button>,
}
render(<SubagentHeaderLineage {...input} />)
@@ -1014,10 +1014,10 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
kind: 'single',
scope: 'session',
summary: 'One breadcrumb title and its lineage controls.',
doc: 'One breadcrumb title and its lineage controls. The owner supplies the\nordinary title as both slot currency and render fallback; an occupant\nmay replace it with one combined navigation control.',
doc: 'One breadcrumb title and its lineage controls. The render site keeps\nthe ordinary title as fallback; an occupant receives plain title data\nand may replace a subagent title with one combined navigation control.',
registerOptions: [],
ownerProps: [
'/** Breadcrumb-title content handed to the optional lineage renderer. */\nexport interface ConversationHeaderLineageOwnerProps {\n /** Session represented by this breadcrumb title. */\n lineageSessionId: SessionId\n /** Ordinary breadcrumb-title node retained when no lineage renderer is installed. */\n title: ReactNode\n /** Display title available to a renderer that combines the title with a control. */\n displayTitle: string\n /** Navigate to an ancestor title when its combined control is clicked. */\n openTitle?: () => void\n}',
'/** Plain breadcrumb data handed to the optional lineage renderer. */\nexport interface ConversationHeaderLineageOwnerProps {\n /** Session represented by this breadcrumb title. */\n lineageSessionId: SessionId\n /** Display title available to a renderer that combines the title with a control. */\n displayTitle: string\n /** Navigate to an ancestor title when its combined control is clicked. */\n openTitle?: () => void\n}',
],
ownerPropsReferences: [
'SessionId',