From 990f700d3cd4c977a5cb1788ef7c9c5d138f918a Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:28:42 +0800 Subject: [PATCH] fix(client): address conversation assembly review feedback --- ...lient-conversation-node-assembly.i18n.yaml | 4 ++-- ...08-09-client-conversation-node-assembly.md | 10 ++++---- ...09-client-conversation-node-assembly.zh.md | 10 ++++---- packages/client/runtime/README.i18n.yaml | 4 ++-- packages/client/runtime/README.md | 2 +- packages/client/runtime/README.zh.md | 2 +- .../client/sessions/conversation-assembler.ts | 2 ++ .../src/client/sessions/conversation.ts | 24 ++++++++++++------- .../client/ui-conversation/README.i18n.yaml | 4 ++-- packages/client/ui-conversation/README.md | 4 ++-- packages/client/ui-conversation/README.zh.md | 4 ++-- .../src/client/contract/slots.ts | 9 ++++--- .../client/conversation-nodes/assistant.ts | 4 ++-- .../chat-snapshot-builder.ts | 5 +++- .../src/client/conversation-nodes/common.ts | 10 ++++++++ .../src/client/conversation-nodes/tool.ts | 4 ++-- .../client/conversation-nodes/turn-tail.ts | 8 ++++--- packages/client/ui-tool/README.i18n.yaml | 4 ++-- packages/client/ui-tool/README.md | 4 ++-- packages/client/ui-tool/README.zh.md | 4 ++-- 20 files changed, 73 insertions(+), 49 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml index 953a049744..264b900b74 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.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/architecture/2026-08-09-client-conversation-node-assembly.md -2026-08-09-client-conversation-node-assembly.md: 0cee5449f651c568dbb87b9cb867eb77b6380184 -2026-08-09-client-conversation-node-assembly.zh.md: 146e8f68a9b1339934040cee62fd46e981145f2a +2026-08-09-client-conversation-node-assembly.md: f9768a652c7b0d2d29939fac83b201bc904e3210 +2026-08-09-client-conversation-node-assembly.zh.md: 408d61f73b3b317214cb10b26f522e7680de3f92 diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md index 0cee5449f6..f9768a652c 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md @@ -148,7 +148,7 @@ The Assembler verifies `node.key === context.key` and `node.target === target`. `current` lets a Definition distinguish "never materialized" from "already materialized and now hidden." Assistant retry and Turn Error suppression use it to avoid illegal Node withdrawal. -A Definition may branch by target to construct different data, while matching, Context identity, and State remain target-neutral. This change registers only the `chat` builder; Trajectory continues to consume the compatibility slice. +A Definition may branch by target to construct different data, while matching, Context identity, and State remain target-neutral. This change registers only the `chat` builder; Trajectory remains on its independent `session-history` fold until it gains a registered target. #### No generic `end()` @@ -306,7 +306,7 @@ Unknown fallback demonstrates Registry ownership: it handles only append-surface The Assembler calls `replace({ nodes, timeline })` on low-frequency complete replacements and `apply({ upserts, timeline })` for ordinary prepend/append flushes. Builders receive only final target Nodes already constructed by Definitions. -[`ChatSnapshotBuilder`](../../../../packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts) maintains `order`, a keyed `nodes` store, the turn/step `locations` index, `timeline`, and the `legacy` slice temporarily consumed by Trajectory. +[`ChatSnapshotBuilder`](../../../../packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts) maintains `order`, a keyed `nodes` store, the turn/step `locations` index, `timeline`, and the `legacy` slice used by StatsLine and mirrored into top-level public compatibility fields. Only a new key or a change to `anchorSeq`, visibility, or Location identity makes a Chat update structural. An ordinary content change does not rebuild `order`; the keyed Node store replaces only that key's value. @@ -328,7 +328,7 @@ When business logic deliberately changes a materialized Node to hidden, it leave The concrete Tool renderer remains governed by the [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md). Tool Definition supplies recursive root/subcall data, and `ui-tool` dispatches concrete presentation by the Tool-name keyed slot. -Trajectory has no independent registered target yet. It continues to consume the legacy slice incrementally derived by the Chat Builder, while Session no longer runs a second transcript fold; a future migration does not change the Event Definition, Context, Reader, or Location contracts. +Trajectory has no registered target and does not consume the Chat Builder's legacy slice. Its activated `SessionHistoryInspection` keeps an independent history fold, while the ordinary Session snapshot no longer runs a second transcript fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; a future Trajectory migration does not change the Event Definition, Context, Reader, or Location contracts. ## Runtime and render path @@ -382,7 +382,7 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping- **Add generic `end()`, prepared, or window-reset lifecycles.** Rejected: businesses have different completion conditions, and a pagination gap is not a business lifecycle. Business Events update State, Location close triggers replay/build, and Reader dependencies own pagination invalidation. -**Register separate Event Definitions for Chat and Trajectory.** Rejected: identity, State, and Location are target-neutral. `buildViewNode(target)` and each Builder express view differences; Trajectory retains a compatibility slice until its actual migration. +**Register separate Event Definitions for Chat and Trajectory.** Rejected: identity, State, and Location are target-neutral. `buildViewNode(target)` and each Builder express view differences; Trajectory's independent history fold remains until it registers its own Builder. **Add a generic layout model above final business Nodes.** Rejected: activity, tail candidacy, and layout enums would centralize current Chat business semantics in the engine again. Final Nodes carry renderer-required data directly and share only identity, ordering, and Location facts. @@ -404,4 +404,4 @@ Steps and Turns become stable homes for cross-business aggregates. Turn Tail and The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definition authors must understand stable IDs, unique starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal. -`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice until Trajectory migrates; built-in Definitions currently remain centralized in `ui-conversation`. These compatibility boundaries do not return business interpretation to Session. +`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, Trajectory still owns an independent history fold, and built-in Definitions currently remain centralized in `ui-conversation`. These compatibility boundaries do not return business interpretation to Session. diff --git a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md index 146e8f68a9..408d61f73b 100644 --- a/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.zh.md @@ -148,7 +148,7 @@ Assembler 校验 Node `key === context.key` 且 Node `target === target`。业 `current` 让 Definition 区分“从未生成”与“已经生成后需要隐藏”。Assistant retry 和 Turn Error suppression 使用它避免非法的 Node 撤回。 -Definition 可以针对 target 分支构造不同 data,但匹配、Context identity 和 State 保持 target-neutral。本次只注册 `chat` builder,Trajectory 仍通过兼容 slice 使用结果。 +Definition 可以针对 target 分支构造不同 data,但匹配、Context identity 和 State 保持 target-neutral。本次只注册 `chat` builder;在拥有注册 target 之前,Trajectory 继续使用独立的 `session-history` fold。 #### 不提供通用 `end()` @@ -306,7 +306,7 @@ Unknown fallback 展示了 Registry ownership:fallback 只处理没有任何 Assembler 低频完整替换时调用 `replace({ nodes, timeline })`;普通 prepend/append flush 调用 `apply({ upserts, timeline })`。Builder 只接收 Definition 已构造完成的 target Nodes。 -[`ChatSnapshotBuilder`](../../../../packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts) 维护 `order`、keyed `nodes` store、turn/step `locations` index、`timeline` 和 Trajectory 临时使用的 `legacy` slice。 +[`ChatSnapshotBuilder`](../../../../packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts) 维护 `order`、keyed `nodes` store、turn/step `locations` index、`timeline`,以及由 StatsLine 使用并镜像到顶层公共兼容字段的 `legacy` slice。 Chat 结构变化只由新 key、`anchorSeq`、visibility 或 Location identity 变化触发。普通内容变化不重建 `order`;keyed Node store 只替换该 key 的 value。 @@ -328,7 +328,7 @@ Assistant streaming 到 final、Tool running 到 settled 只更新同一个 Seat 具体 Tool renderer 仍由 [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md) 约束。Tool Definition 只交付递归 root/subcall data,`ui-tool` 再按 Tool name keyed slot 分发具体表现。 -Trajectory 尚未注册独立 target。它继续消费 Chat Builder 增量派生的 legacy slice,Session 不再运行第二套 transcript fold;未来迁移不改变 Event Definition、Context、Reader 或 Location 契约。 +Trajectory 尚未注册 target,也不消费 Chat Builder 的 legacy slice。它已激活的 `SessionHistoryInspection` 继续维护独立 history fold,而普通 Session snapshot 不再运行第二套 transcript fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slice;未来迁移 Trajectory 不改变 Event Definition、Context、Reader 或 Location 契约。 ## Runtime and render path @@ -382,7 +382,7 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏 **增加通用 `end()`、prepared 或 window reset 生命周期。** 拒绝:不同业务完成条件不同,分页缺口也不是业务生命周期。业务 Event 更新 State,Location close 触发 replay/build,Reader dependency 负责补页失效。 -**为 Chat 与 Trajectory 注册两套 Event Definition。** 拒绝:identity、State 和 Location 与 target 无关。视图差异由 `buildViewNode(target)` 和各自 Builder 表达;Trajectory 在真正迁移前保留兼容 slice。 +**为 Chat 与 Trajectory 注册两套 Event Definition。** 拒绝:identity、State 和 Location 与 target 无关。视图差异由 `buildViewNode(target)` 和各自 Builder 表达;Trajectory 在注册自己的 Builder 之前继续使用独立 history fold。 **在最终业务 Node 上再叠一层通用 layout model。** 拒绝:activity、tail candidacy 和 layout enum 会把当前 Chat 的业务语义重新集中到引擎。最终 Node 直接携带 renderer 所需 data,只共享 identity、排序和 Location 事实。 @@ -404,4 +404,4 @@ Step/Turn 成为业务间共享聚合的稳定宿主。Turn Tail 和 Deliverable 代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约,UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。Definition 作者必须理解稳定 ID、唯一 start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。 -`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 在 Trajectory 迁移前仍维护 legacy slice;内建 Definitions 暂时集中在 `ui-conversation`。这些是兼容边界,不把业务解释权交还给 Session。 +`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试,而不是能力隔离。Registry 变化仍是低频完整 rebuild;Chat Builder 继续为 StatsLine 和顶层公共字段维护 legacy slice,Trajectory 继续拥有独立 history fold,内建 Definitions 暂时集中在 `ui-conversation`。这些是兼容边界,不把业务解释权交还给 Session。 diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index 4aed1d0f36..d47266cd8a 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/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/runtime/README.md -README.md: b2bb06e50ecd791d74cb609404dd219e5a21913e -README.zh.md: 72cd99ac765875c828cc9163d978e0b8fb7f44e4 +README.md: 46a383f22032c8da7e4bb9b5fb445980661c670f +README.zh.md: 73578a8f249472abfdd7c539e3eaee6de767ad60 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index b2bb06e50e..46a383f220 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -38,7 +38,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and Each `Session` gives its contiguous event window to a `ConversationNodeAssembler`. Plugins register business Definitions that map one event to a stable `{kind, id}`, create State at the unique start event, fold correlated updates, and build final nodes for registered view targets. The assembler owns the Context index, read-only predecessor lookup, and a reference-stable Turn/Step Location index. A live append evaluates each Definition once and updates only the matched Context; loading an older page preserves existing Context and node identities, matches only the newly prepended events, and replays Contexts whose predecessor or Location facts changed. Full replacement is reserved for open, resync, and gap repair. -`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. `ConversationSnapshot.nodes`, `partial`, and `runningCalls` are compatibility slices derived from the same materialized Chat nodes for consumers that have not moved to `ConversationSnapshot.chat`; Session does not run a second business fold. +`ui-conversation` registers the built-in Chat Definitions and the keyed Chat snapshot builder. Append-origin user, assistant, and Tool results remain the human record; model-only replacement copies stay out, except that a compaction checkpoint becomes its own marker and resolves missing summary provenance when an older page supplies it. Durable inbox splice Contexts classify next-step user messages as steering without making inbox state a Session special case. Context messages retain producer provenance and form. StatsLine reads `ConversationSnapshot.chat.legacy.nodes`, while Session mirrors that legacy slice into the top-level `nodes`, `partial`, and `runningCalls` public compatibility fields without running a second business fold. Trajectory consumes neither compatibility surface; its activated `session-history` inspection keeps an independent fold until it gains its own registered target. The Chat builder keeps one mutable keyed store per Session. Content updates notify only the affected node key, structural changes rebuild order and Location membership, and a prepend adds rows without replacing existing keyed values. Assistant chunks update Definition State for every event but request at most one materialization per animation frame; final messages and Turn/Step closure publish immediately. See the [client Tool presentation decision](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md). diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index 72cd99ac76..73578a8f24 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -38,7 +38,7 @@ SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 每个 `Session` 都把连续事件窗口交给 `ConversationNodeAssembler`。插件注册业务 Definition,把单个事件映射为稳定的 `{kind, id}`,在唯一 start 事件处创建 State,折叠有关联的 update,再为已注册的视图目标构造最终节点。Assembler 负责 Context 索引、只读前序 Context 查询,以及引用稳定的 Turn/Step Location 索引。实时 append 只对每个 Definition 求值一次,并且只更新命中的 Context;加载更早分页时保留已有 Context 与节点身份,只匹配新 prepend 的事件,并重放前序依赖或 Location 事实发生变化的 Context。完整替换仅用于 open、resync 和 gap repair。 -`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chat,compaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering,无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。`ConversationSnapshot.nodes`、`partial` 和 `runningCalls` 是从同一批已物化 Chat 节点派生的兼容切片,供尚未迁移到 `ConversationSnapshot.chat` 的消费者使用;Session 不再运行第二套业务 fold。 +`ui-conversation` 注册内建 Chat Definition 与 keyed Chat snapshot builder。append 来源的 user、assistant 和 Tool result 构成人类可见记录;仅供模型使用的 replacement 副本不进入 Chat,compaction 检查点除外,它会成为独立标记,并在更早分页补齐 summary 溯源后更新。持久 inbox splice Context 能把 next-step 用户消息判定为 steering,无须让 inbox 状态成为 Session 特例。上下文消息保留生产者 provenance 与 form。StatsLine 读取 `ConversationSnapshot.chat.legacy.nodes`;Session 则把该 legacy slice 镜像到顶层 `nodes`、`partial` 和 `runningCalls` 公共兼容字段,无须运行第二套业务 fold。Trajectory 不消费这两种兼容表面;在它获得独立注册 target 之前,已激活的 `session-history` inspection 继续维护独立 fold。 Chat builder 为每个 Session 保留一个 mutable keyed store。内容更新只通知受影响的 node key;结构变化才重建顺序和 Location 成员关系;prepend 只增加行,不替换既有 keyed value。每个 Assistant chunk 都会更新 Definition State,但最多每个 animation frame 请求一次物化;final message 与 Turn/Step 关闭会立即发布。参见 [Client Tool 展示所有权决策](../../../.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.md)。 diff --git a/packages/client/runtime/src/client/sessions/conversation-assembler.ts b/packages/client/runtime/src/client/sessions/conversation-assembler.ts index 3a0c94ba3c..811bd43ebf 100644 --- a/packages/client/runtime/src/client/sessions/conversation-assembler.ts +++ b/packages/client/runtime/src/client/sessions/conversation-assembler.ts @@ -743,6 +743,8 @@ export class ConversationNodeAssembler { context.locationData[scope] = data if (data !== null) entries.push({ owner: context.key, data }) } + // Turn publishers may read Step data from this same flush, so each phase + // installs the cumulative replacement before the next phase builds. this.locationIndex.replaceData(entries) } } diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index 99daa7c8a5..628fe89aa8 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -1,7 +1,9 @@ // ConversationSnapshot / ConversationNode: the only data shape the logic layer feeds the UI. -// Immutability contract: every change swaps the top-level object; unchanged -// substructures keep their references (the React.memo premise). callId/approvalId stay plain -// string here (narrow to real brands when convenient). +// Publication contract: every change swaps the top-level object; unchanged +// substructures keep their references (the React.memo premise). Chat node and +// Location stores are stable live readers, so old snapshots are not time-point +// views. callId/approvalId stay plain string here (narrow to real brands when +// convenient). import type { CommandId } from '@deepseek-ai/dsh-commands/brand' import type { MessageId } from '@deepseek-ai/dsh-llm/brand' @@ -336,7 +338,10 @@ export interface PromptError { error: RpcError } -/** Stable per-key reader for final Chat view Nodes. */ +/** + * Stable live per-key reader. An old ChatSnapshot observes later flushes + * through this store. + */ export interface ChatNodeStore { /** @param key - stable Conversation Context key. @returns current Node, when visible or hidden. */ get(key: string): ChatConversationViewNode | undefined @@ -344,7 +349,10 @@ export interface ChatNodeStore { values(): readonly ChatConversationViewNode[] } -/** Stable per-Location membership index for turn-local and step-local consumers. */ +/** + * Stable live Location index. An old ChatSnapshot observes later membership + * changes through this index. + */ export interface ChatLocationNodeIndex { /** @param turn - owning turn. @returns ordered Chat Node keys in the turn. */ getTurn(turn: number): readonly string[] @@ -352,7 +360,7 @@ export interface ChatLocationNodeIndex { getStep(turn: number, step: number): readonly string[] } -/** Temporary projection consumed by Trajectory and unmigrated readers. */ +/** Compatibility projection backing StatsLine and the legacy top-level snapshot fields. */ export interface LegacyConversationSlice { readonly nodes: readonly ConversationNode[] readonly turnTimings: ReadonlyMap @@ -361,7 +369,7 @@ export interface LegacyConversationSlice { readonly runningCalls: readonly RunningToolCall[] } -/** Incremental Chat target snapshot: stable keyed stores plus structural order. */ +/** Incremental Chat publication with immutable order and stable live keyed readers. */ export interface ChatSnapshot { readonly order: readonly string[] readonly nodes: ChatNodeStore @@ -399,7 +407,7 @@ export interface ConversationSnapshot { sessionId: SessionId /** Final Chat target assembled from independently registered business Definitions. */ chat: ChatSnapshot - /** Legacy Trajectory slice derived from the registered Chat Definitions. */ + /** Legacy top-level compatibility field mirrored from the registered Chat Definitions. */ nodes: readonly ConversationNode[] /** Exact in-window `turn/start` time and optional matching `turn/end` time. */ turnTimings: ReadonlyMap diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 2066b22ec5..bb15c65029 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/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-conversation/README.md -README.md: 911bca28dcfb31b1d8ef9ea5458a0d2017d8b31d -README.zh.md: 25574421bbcc3992188e378a49a69acf25744af7 +README.md: 3570e814609af0bc4e7d048a55205af756fae843 +README.zh.md: e37f867c72d83b47924e87261e6ee5ff8b374a6f diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 911bca28dc..3570e81460 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -20,7 +20,7 @@ Logged non-user messages render as a default-collapsed disclosure whose header n A Think row stays collapsed by default and exposes live reasoning throughput without expanding the chain of thought: while its reasoning block is the streaming tail, the summary switches from the settled first line to the latest non-blank line and its one-line scrollport follows each delta to the inline end. Expanding the row removes the moving summary and leaves the full reasoning in ordinary page flow, so page reading never fights an internal follower; settlement restores the stable first-line summary at the left edge ([decision](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md)). -The chat view keeps Tool placement but delegates Tool presentation. It passes each ordered root call through `conversation.chat.tool`, and the details shell passes the selected call through `conversation.details.tool`. The assembled Web bundle fills the whole-Tool seat with [`ui-tool`](../ui-tool/README.md), which selects Runtime-projected Code Dispatch children and owns root/child composition, per-name dispatch, generic rendering, and render-intent cards; the details seat alone retains a raw-result fallback when that renderer is absent. +The chat view keeps Tool placement but delegates Tool presentation. Each ordered `tool-call` Conversation Node dispatches through the matching key of `conversation.chat.node`, while the details shell passes the selected call through `conversation.details.tool`. The assembled Web bundle registers [`ui-tool`](../ui-tool/README.md) for that Chat Node key; it renders the Runtime-projected recursive root/child tree and owns per-name dispatch, generic rendering, and render-intent cards. The details seat alone retains a raw-result fallback when that renderer is absent. The chat flow projects consecutive model-retry nodes across retry turns into one stable, muted status row updated to the latest attempt; every retry event remains in the runtime snapshot and session log. Its frontend countdown anchors the scheduled delay to client receipt, avoiding host/browser clock skew, rounds remaining time up to seconds, and has a one-second floor. The latest unresolved retry uses a left-to-right text shimmer. Subsequent turn facts distinguish an attempt that started from one cancelled during backoff, while the Host running bit only controls the live animation; the row then shows a static completed or cancelled label. Normal policy rows show the finite retry maximum; always policy rows show `∞`. Activating the row reveals the latest exact retry delay and failure message. The client runtime removes each failed step's streaming tail before its retry node arrives, while the status remains visible after a later attempt succeeds. An unretried terminal failure renders as a persistent inline status at its turn boundary, showing the display-safe durable message and optional error code without offering an action the Host cannot fulfill; AUTH copy never echoes provider-supplied credential fragments. @@ -40,7 +40,7 @@ The chat stats line takes its token accounting from the generic token-meter `tok `src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` export surface contains only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations. -A finished turn ends with a turn-tail hole: the chat view renders the `conversation.chat.turnTail` list slot between the closing assistant's body and its IconActions, once per turn at the seq `assistantActionsSeqs` elects, dispatching `TurnTailOwnerProps` (the snapshot nodes, the closing seq, and the tool rows' `openFile`). This package owns only the hole; the produced-files row that fills it — derivation from the mutation tools' `locations`, the chip cap, the copy — lives in `@deepseek-ai/dsh-client-ui-deliverables`, so composing that plugin out of cordis.yml turns the surface off while the hole renders empty at zero cost. The closing prose participates through the same off switch: the chat view asks the optional `chatFileMentions` service (ctx.get; provided by the same plugin) for a closing message's inline-code vocabulary and threads the result into MarkdownText's `fileMentions` contract — an absent service leaves the prose inert. +A finished turn materializes one ordered `turn-tail` Conversation Node. Its engine-owned `TurnLocation` supplies the closing Assistant and Turn data; the renderer places the `conversation.chat.turnTail` chain before that node's IconActions and dispatches `TurnTailOwnerProps` containing the Turn, closing seq, and `openFile`. This package owns only the hole; `@deepseek-ai/dsh-client-ui-deliverables` accumulates mutation-tool `locations` into Turn data and owns the produced-files row, chip cap, and copy, so composing that plugin out of cordis.yml turns the surface off while the hole renders empty at zero cost. The closing prose participates through the same off switch: the chat view asks the optional `chatFileMentions` service (ctx.get; provided by the same plugin) for a closing message's inline-code vocabulary and threads the result into MarkdownText's `fileMentions` seam — an absent service leaves the prose inert. ## Model Experience diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 25574421bb..e37f867c72 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -18,7 +18,7 @@ Think 行默认保持折叠,并在不展开思维链的情况下暴露实时推理(reasoning)吞吐:当推理块是流式输出尾部时,摘要从结算后的首行切换到最新的非空行,其单行滚动区会随每个 delta 追到行内末端。展开该行会移除移动摘要,让完整推理进入普通页面流,因此页面阅读不会与内部跟随器争夺滚动;结算后恢复左对齐的稳定首行摘要([决策](../../../.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.md))。 -聊天视图保留 Tool 的消息流位置,但委托其展示。它通过 `conversation.chat.tool` 传递每个已排序的 root call;详情壳层则通过 `conversation.details.tool` 传递当前选中的调用。组装后的 Web bundle 由 [`ui-tool`](../ui-tool/README.md) 填充整体 Tool 席位,并由后者选择 Runtime 已投影的 Code Dispatch 子调用,负责 root/child 编排、按名称分发、通用展示和 render-intent 卡片;只有详情席位会在该 renderer 缺席时保留 raw-result fallback。 +聊天视图保留 Tool 的消息流位置,但委托其展示。每个已排序的 `tool-call` Conversation Node 都通过 `conversation.chat.node` 的同名 key 分发;详情壳层则通过 `conversation.details.tool` 传递当前选中的调用。组装后的 Web bundle 为该 Chat Node key 注册 [`ui-tool`](../ui-tool/README.md),由后者渲染 Runtime 已投影的递归 root/child 树,并负责按名称分发、通用展示和 render-intent 卡片;只有详情席位会在该 renderer 缺席时保留 raw-result fallback。 聊天流会将跨重试轮次连续出现的模型重试节点投影为一个稳定的弱化状态行,并用最新一次尝试更新该行;每个重试事件仍保留在运行时快照与会话日志中。前端倒计时以客户端收到事件的时刻为计划延迟的起点,避免 Host 与浏览器的时钟偏差;剩余时间向上取整到秒,且下限为 1 秒。最近一次尚未完成的重试会显示从左到右的文字渐变动画。后续轮次事实用于区分已开始的尝试与在退避期间取消的尝试,Host 的 running 位只控制实时动画;随后该行会显示静态的已完成或已取消标签。normal 策略行显示有限重试上限;always 策略行显示 `∞`。激活该行会显示最近一次重试的精确延迟和失败消息。客户端运行时会在相应重试节点到达前移除每个失败步骤的流式输出尾部;后续某次尝试成功后,该状态仍保持可见。未进入重试的终态失败会在其轮次边界渲染为持久的内联状态,展示适合显示的持久消息与可选错误码,但不会提供 Host 无法兑现的操作;AUTH 文案绝不会回显提供方给出的凭据片段。 @@ -40,7 +40,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu `src/client/` 按领域组织。`contract/` 是 slot 声明、组合 props 与跨领域类型的共享表层;`skeleton/`、`chat/`、`input/`、`queue/` 和 `settings/` 保持内部实现,`apply.ts` 是它们的组装点。`/client` 导出表层只包含 loader entry、service class 和 contract 类型;组件与 store factory 经 slot 注册抵达页面。 -完成的一轮以一个 turn-tail 空位收尾:chat 视图在收尾 assistant 正文与其 IconActions 之间渲染 `conversation.chat.turnTail` list slot,每轮一次、位于 `assistantActionsSeqs` 选出的 seq,派发 `TurnTailOwnerProps`(快照节点、收尾 seq,以及工具行的 `openFile`)。本包只拥有空位;填充它的产物行——从改写工具 `locations` 的派生、chip 上限、文案——都在 `@deepseek-ai/dsh-client-ui-deliverables` 里,因此把那个插件从 cordis.yml 中组合掉即可关闭该交互面,空位以零成本渲染为空。收尾正文经由同一个开关参与其中:chat 视图向可选的 `chatFileMentions` service(ctx.get;由同一插件提供)索取收尾消息的行内代码词表,并把结果接进 MarkdownText 的 `fileMentions` 约定——service 缺席时正文保持死文本。 +完成的一轮会物化一个有序的 `turn-tail` Conversation Node。它由引擎维护的 `TurnLocation` 提供收尾 Assistant 和 Turn data;renderer 在该 Node 的 IconActions 之前渲染 `conversation.chat.turnTail` chain,并派发包含 Turn、收尾 seq 和 `openFile` 的 `TurnTailOwnerProps`。本包只拥有空位;`@deepseek-ai/dsh-client-ui-deliverables` 把改写工具的 `locations` 累积到 Turn data,并拥有产物行、chip 上限和文案,因此把该插件从 cordis.yml 中组合掉即可关闭该交互面,空位以零成本渲染为空。收尾正文经由同一个开关参与其中:chat 视图向可选的 `chatFileMentions` service(ctx.get;由同一插件提供)索取收尾消息的行内代码词表,并把结果接进 MarkdownText 的 `fileMentions` seam——service 缺席时正文保持死文本。 ## 模型体验 diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 474e7f9db5..eec5450e2d 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -56,11 +56,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { */ 'conversation.chat.commandview': { kind: 'keyed'; scope: 'session'; owner: CommandRowOwnerProps } /** - * The chat view's turn-tail chain: rendered between a closing assistant - * message's body and its IconActions footer, once per turn (the render - * site elects the closing seq). Entries derive a match from the owner - * currency before mounting, so presentation components never mount only - * to return null; an all-declined chain renders nothing. + * The completed Turn Node's extension chain, rendered before that Node's + * IconActions. Entries derive a match from the engine-owned Turn and + * closing seq before mounting, so presentation components never mount + * only to return null; an all-declined chain renders nothing. */ 'conversation.chat.turnTail': { kind: 'chain'; scope: 'session'; owner: TurnTailOwnerProps } /** Selected Tool call output inside the details panel. */ diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts b/packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts index 2bdf960226..8a487c89ed 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/assistant.ts @@ -7,7 +7,7 @@ import { emptyAssistantBlock, isAppendSurfaceEvent, isTokenDelta, toAssistantBlock, toAssistantBlocks, } from '@deepseek-ai/dsh-client-runtime/client' import type { AssistantChatData } from '../contract/chat-nodes.ts' -import { chatNode } from './common.ts' +import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts' declare module '@deepseek-ai/dsh-client-ui-conversation/client' { interface ChatNodeDataMap { @@ -169,7 +169,7 @@ function finalNode( if (boundary === undefined || !hasInterruptionEvidence(blocks)) return undefined return { kind: 'assistant', - seq: boundary.seq - 0.9, + seq: boundary.seq + CHAT_SYNTHETIC_SEQ_OFFSETS.interruptedAssistant, time: boundary.time, turn: state.turn, step: state.step, diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts b/packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts index f9491d2d16..f417c33b75 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/chat-snapshot-builder.ts @@ -154,6 +154,9 @@ const EMPTY_CONTRIBUTION: LegacyContribution = { function legacyContribution(raw: ChatConversationViewNode): LegacyContribution { const node = raw as ChatNode + // Content-free settled Assistants remain in the finalized compatibility + // stream so StatsLine preserves its pre-assembly step counts; hidden running + // attempts have no final Node to contribute. if (raw.visibility !== 'visible' && node.kind !== 'assistant-step') return EMPTY_CONTRIBUTION switch (node.kind) { case 'user': @@ -221,7 +224,7 @@ function sameContribution(left: LegacyContribution | undefined, right: LegacyCon && sameReferences(left.nodes, right.nodes) } -/** Incremental compatibility projection retained solely for unmigrated Trajectory consumers. */ +/** Incremental compatibility projection for StatsLine and legacy top-level snapshot fields. */ class LegacySliceBuilder { private readonly contributions = new Map() private readonly finalizedContributions = new Map() diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/common.ts b/packages/client/ui-conversation/src/client/conversation-nodes/common.ts index 8e1d9d7bd4..b6bd01930b 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/common.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/common.ts @@ -5,6 +5,16 @@ import type { ChatNode, ChatNodeDataMap, ChatNodeKind, } from '../contract/chat-nodes.ts' +/** + * Relative positions in one durable event's seq neighborhood: interrupted + * Assistant, its follow-up Nodes, then follow-ups to an ordinary final. + */ +export const CHAT_SYNTHETIC_SEQ_OFFSETS = { + interruptedAssistant: -0.9, + interruptedFollowup: -0.8, + finalizedFollowup: 0.1, +} as const + /** * Resolve one Context's best currently loaded event Location. * @param context - assembled business Context. diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts b/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts index 04a5770f5a..23201fec3e 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/tool.ts @@ -5,7 +5,7 @@ import type { } from '@deepseek-ai/dsh-client-runtime/client' import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-client-runtime/client' import type { ToolChatData } from '../contract/chat-nodes.ts' -import { chatNode } from './common.ts' +import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts' declare module '@deepseek-ai/dsh-client-ui-conversation/client' { interface ChatNodeDataMap { @@ -190,7 +190,7 @@ function projectBlock( ? sameReferences(block.subCalls, children) ? block : { ...block, subCalls: children } : { kind: 'tool-result', - seq: interruptedAt.seq - 0.8, + seq: interruptedAt.seq + CHAT_SYNTHETIC_SEQ_OFFSETS.interruptedFollowup, time: interruptedAt.time, callId: block.callId, call: { name: block.name, argsRaw: block.argsRaw }, diff --git a/packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts b/packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts index 346bd41bdf..51cb348e6a 100644 --- a/packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts +++ b/packages/client/ui-conversation/src/client/conversation-nodes/turn-tail.ts @@ -7,7 +7,7 @@ import type { AssistantChatData, FinalAssistantChatData, TurnTailChatData, } from '../contract/chat-nodes.ts' import { deriveTurnMetrics } from '../chat/turn-metrics.ts' -import { chatNode } from './common.ts' +import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts' declare module '@deepseek-ai/dsh-client-ui-conversation/client' { interface ChatNodeDataMap { @@ -85,7 +85,9 @@ function closingAnchor(context: ConversationNodeContext): number } if (event.type === 'assistant/message') { steps.set(coordinates.step, { streamedText: false, finalized: true }) - if (hasTextAssistant(event)) anchor = event.seq + 0.1 + if (hasTextAssistant(event)) { + anchor = event.seq + CHAT_SYNTHETIC_SEQ_OFFSETS.finalizedFollowup + } continue } if ((event.type as string) === 'llm/retry') { @@ -93,7 +95,7 @@ function closingAnchor(context: ConversationNodeContext): number continue } if (event.type === 'step/end' && previous.streamedText && !previous.finalized) { - anchor = event.seq - 0.8 + anchor = event.seq + CHAT_SYNTHETIC_SEQ_OFFSETS.interruptedFollowup } } return anchor diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index ca5f3cd25c..84c6fa42af 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/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-tool/README.md -README.md: d6bc0f248cffaf4c65ecf6d97c7a4afb17fc8941 -README.zh.md: 6b6e2a153be6a3cad5b57379de6fdc0cd4a58ea4 +README.md: b33aef86a6ad161f70105968f308c5a587c25c24 +README.zh.md: 680a5149187bc43513c74134a9a58c4680af854c diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index d6bc0f248c..b33aef86a6 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Client Tool presentation plugin. `ui-conversation` supplies one ordered root call through `conversation.chat.tool`; this package renders that root and its Code Dispatch children, then dispatches every atomic call through the keyed `tool.call.toolview` slot. Unregistered Tool names use the generic card. +Client Tool presentation plugin. `ui-conversation` dispatches each ordered `tool-call` Conversation Node through the matching key of `conversation.chat.node`; this package renders its root and Code Dispatch children, then dispatches every atomic call through the keyed `tool.call.toolview` slot. Unregistered Tool names use the generic card. Business UI packages register only their wire Tool names and atomic views. They do not pair Session events, rebuild the transcript, or own root/subcall topology. The Runtime remains authoritative for call/result pairing, lifecycle, and recursive `subCalls` projection; the conversation view remains authoritative for ChatFlow placement. @@ -10,7 +10,7 @@ Business UI packages register only their wire Tool names and atomic views. They `ToolCallTree` receives one root `ToolCallBlock` that already contains recursive `subCalls`, selection state, the session `cwd`, and Host callbacks for opening files and inspecting calls. It recursively walks the standard call blocks and sends the root and children at every depth through the same atomic dispatch path, without subscribing to a separate parent-to-children map. -Each root and child wrapper preserves the `conversation.chat.tool` call-anchor DOM contract used for paging and selection. +Each root and child wrapper preserves the `data-chat-anchor-key="call:"` and `data-chat-call-id` DOM contract used for paging and selection. The package also fills `conversation.details.tool` with `ToolDetails`. The row and details renderers share the same pure card models for `terminal`, `read`, `diff`, `search`, and `web` render intents. Unknown intent tags and malformed wire card data fall back to flattened Tool result text. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 6b6e2a153b..680a514918 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -Client Tool 展示插件。`ui-conversation` 通过 `conversation.chat.tool` 交付一个已经排好位置的 root call;本包渲染该 root 及其 Code Dispatch 子调用,并把每个原子调用通过 keyed slot `tool.call.toolview` 分发。没有注册的 Tool 名称使用通用卡片。 +Client Tool 展示插件。`ui-conversation` 通过 `conversation.chat.node` 的同名 key 分发每个已排序的 `tool-call` Conversation Node;本包渲染其中的 root 及其 Code Dispatch 子调用,并把每个原子调用通过 keyed slot `tool.call.toolview` 分发。没有注册的 Tool 名称使用通用卡片。 业务 UI 包只注册 wire Tool 名称和原子视图,不配对 Session Event、不重建 transcript,也不拥有 root/subcall 拓扑。Runtime 继续负责 call/result 配对、生命周期和递归 `subCalls` 投影;conversation view 继续负责 ChatFlow 位置。 @@ -10,7 +10,7 @@ Client Tool 展示插件。`ui-conversation` 通过 `conversation.chat.tool` 交 `ToolCallTree` 接收一个已经包含递归 `subCalls` 的 root `ToolCallBlock`、selection 状态、会话 `cwd`,以及用于打开文件和检查调用的 Host 回调。它递归遍历标准 call block,让 root 与任意深度的 child 经过同一条原子分发路径,不再订阅独立的 parent-to-children map。 -每个 root 和 child wrapper 都保留 `conversation.chat.tool` 的 call-anchor DOM 约定,供分页和 selection 使用。 +每个 root 和 child wrapper 都保留 `data-chat-anchor-key="call:"` 与 `data-chat-call-id` DOM 约定,供分页和 selection 使用。 本包还通过 `ToolDetails` 填充 `conversation.details.tool`。行 renderer 与详情 renderer 为 `terminal`、`read`、`diff`、`search` 和 `web` render intent 共用同一组纯 card model。本版本不认识的 intent 标签和格式错误的 wire card 数据都会回退为压平的 Tool result 文本。