fix(client): address remaining conversation review feedback

This commit is contained in:
imccyu
2026-08-09 19:12:54 +08:00
parent 27682b9384
commit e11f630fcd
11 changed files with 47 additions and 18 deletions

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 .agents/notes/implemented/architecture/2026-08-09-client-conversation-node-assembly.md
2026-08-09-client-conversation-node-assembly.md: 9952f8fac0f2a3b0fe72cc97ae9a8fb4f96ace08
2026-08-09-client-conversation-node-assembly.zh.md: f27a82459cdacdb4a2da27cf12b30c93f2dfdf64
2026-08-09-client-conversation-node-assembly.md: 16a39539064644e5467f701789a7e2ef1f7ff172
2026-08-09-client-conversation-node-assembly.zh.md: 0e0fbdf8f3320393022528e6e3fe2cf0d492a1d3

View File

@@ -394,6 +394,8 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping-
A new business node can register its matcher, State transitions, optional Location data, final target Node, and renderer locally without changing Session's business switch. `ChatNodeDataMap` and the Location data maps let a business package merge strongly typed data into the contract; every related Event must still expose a stable ID derivable from that Event alone.
Host business packages declaration-merge their durable Event members into `@deepseek-ai/dsh-session/types`, while Client Definitions type-only import the corresponding business package `/types` subpaths. Augmenting the declaring interface rather than a re-export barrel gives the independent Host and Client TypeScript programs the same Event narrowing without pulling Host runtime into the Client graph.
Initial tail, older prepend, and live append share one set of Context invariants. Missing starts, Reader window gaps, unknown Locations, and high-frequency deltas are explicit engine states and require no direction-specific business cache.
Append does not scan historical Contexts; prepend replays only Contexts whose Matches, Locations, or Reader answers actually changed. A structural Chat change may still recompute visible order and indexes, but does not rerun unrelated business folds or replace unchanged Node identity.

View File

@@ -394,6 +394,8 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
新增业务节点可以局部注册自己的 matcher、State 转换、可选 Location data、最终 target Node 和 renderer不再修改 Session 的业务 switch。`ChatNodeDataMap` 和 Location data maps 允许业务 package 通过 declaration merging 合入强类型 data所有相关 Event 仍须暴露可单 Event 推导的稳定 ID。
Host 业务 package 把自己的持久 Event 成员 declaration-merge 到 `@deepseek-ai/dsh-session/types`Client Definition 则通过对应业务 package 的 `/types` 子路径进行 type-only import。增强实际声明接口而不是重导出 barrel使 Host 和 Client 的独立 TypeScript Program 都能获得相同的 Event narrowing同时不把 Host runtime 带入 Client 图。
初始尾页、older prepend 和 live append 共享一套 Context 不变量。缺 start、Reader window gap、Location unknown 以及高频 delta 都是引擎明确表达的状态,不需要业务另建方向相关 cache。
Append 不扫描历史 Contextprepend 只 replay Match、Location 或 Reader 答案真正受影响的 Context。Chat 结构变化仍可能重算 visible order 和索引,但不会重跑无关业务 fold 或替换未变化 Node identity。

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 docs/subsystems/compaction.md
compaction.md: 1aca48d677a83a488d9590edad3f8a68ca662b70
compaction.zh.md: e2cfabc0891126d9f2165e47dffcf485f4a8a339
compaction.md: 11ee6eae797db1c57bf2ffbb90f1f37f47b8400c
compaction.zh.md: a560e47dbb7aa92f0757179534803bbcaaebc6ba

View File

@@ -20,7 +20,7 @@ The lock brackets the **whole** operation: `compact/start` is appended first, th
The markers are lock time points, not an exclusive container. An unrelated idle injection can appear between a standalone manual start and end while summarization is pending. The manual path revalidates only its selected positional span, so that injected context survives after the replacement checkpoint. A live unmatched start blocks every entry point; an unmatched start before a newer `session/end-seed` is stale evidence from a prior lifecycle and is ignored.
These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes.
These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes.
## `CompactionResult`

View File

@@ -20,7 +20,7 @@
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。
这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
## `CompactionResult`

View File

@@ -9,9 +9,11 @@ type TurnTailNodeViewProps = ChatNodeViewProps<'turn-tail'> & PropsRenderSlots<'
/** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */
export const TurnTailNodeView = memo(function TurnTailNodeView({
node, openFile, forkAt, renderSlotChain, t,
node, openFile, forkAt, renderSlotChain, t, useSession,
}: TurnTailNodeViewProps) {
const data = node.data
const hasLaterChatNode = useSession(snapshot =>
snapshot.chat.locations.getTurn(data.turn).at(-1) !== node.key)
const turn = node.location.kind === 'turn' || node.location.kind === 'step'
? node.location.turn
: undefined
@@ -34,7 +36,7 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
tokensPerSecond={data.tokensPerSecond}
clock="end"
onBranch={() => { forkAt(closing.finalNode.seq) }}
branchUnavailable={data.branchUnavailable}
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
className={css.actions}
t={t}
/>

View File

@@ -57,7 +57,7 @@ export interface TurnTailChatData {
readonly time: number
/** Last finalized content-bearing Assistant in this Turn. */
readonly closing: FinalAssistantChatData | null
/** Whether later Assistant/Step material makes the closing seq non-tail. */
/** Whether non-rendered later evidence makes the closing seq non-tail. */
readonly branchUnavailable: boolean
readonly ttftMs?: number
readonly tokensPerSecond?: number

View File

@@ -181,14 +181,12 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
if (key !== 'conversation.chat.node') return opts?.fallback ?? null
const nodeOwner = owner as RoutedChatNodeOwner
const nodeKey = opts?.hookContext as string | undefined
const useTurnData = ((dataKey: string) => {
return props.useSession((snapshot) => {
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
return location?.kind === 'turn' || location?.kind === 'step'
? location.turn.data.get(dataKey as never)
: undefined
})
}) as UseChatNodeTurnData
const useTurnData: UseChatNodeTurnData = dataKey => props.useSession((snapshot) => {
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
return location?.kind === 'turn' || location?.kind === 'step'
? location.turn.data.get(dataKey)
: undefined
})
const nodeProps = <Kind extends ChatNode['kind']>(): ChatNodeViewProps<Kind> => (
{ ...props, ...nodeOwner, useTurnData } as unknown as ChatNodeViewProps<Kind>
)
@@ -722,6 +720,28 @@ describe('ChatView', () => {
expect(h.forkAt.mock.calls).toEqual([[2]])
})
it('disables fork when the indexed Turn has a later steering Node', () => {
const base = chatSnapshotFixture({
nodes: [user(1, 'question'), assistant(2, 'answer')],
turnEnds: new Map([[1, 4]]),
})
const chat = {
...base,
locations: {
getTurn: (turn: number) => turn === 1
? [...base.locations.getTurn(turn), 'fixture:steering:later']
: base.locations.getTurn(turn),
getStep: (turn: number, step: number) => base.locations.getStep(turn, step),
},
}
const h = makeHarness({ chat })
const view = render(<h.ChatView {...h.props} />)
const branch = view.getByRole('button', { name: '在新对话中分支' })
expect(branch.getAttribute('aria-disabled')).toBe('true')
fireEvent.click(branch)
expect(h.forkAt).not.toHaveBeenCalled()
})
it('keeps final content actions but disables branch when Tool and interrupted Think follow it', () => {
const interruptedThink: AssistantMessageNode = {
kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2,

View File

@@ -36,7 +36,9 @@ interface DeliverablesState extends DeliverablesTurnData {
* Paths a call view reports having created or changed, by render intent rather
* than tool name: a diff card, or a generic card whose kind is `edit` (the
* shape `str_replace_editor`'s insert presents). Every other card produces
* nothing to open — a read looked, a delete removed, a terminal ran.
* nothing to open — a read looked, a delete removed, a terminal ran. Only
* root call views enter this Turn accumulator; nested Code Mode dispatches
* preserve the pre-assembly behavior and do not contribute independently.
*/
function producedPaths(view: ToolResultNode['callView']): readonly string[] {
if (view === null) return []

View File

@@ -211,6 +211,7 @@ export default defineConfig({
'packages/client/ui-workspace/src/client/index.ts',
'packages/client/test-runtime/src/translate.ts',
'packages/client/ui-primitives/src/JsonTree.tsx',
'packages/client/ui-deliverables/src/client/turn-deliverables.ts',
// Typert generator: correctness is pinned by its fixture suites and
// the byte-for-byte catalog reproduction test; per-file coverage
// would put whole-workspace compiler analysis under v8