fix(client): address remaining conversation review feedback
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# 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.md: 16a39539064644e5467f701789a7e2ef1f7ff172
|
||||||
2026-08-09-client-conversation-node-assembly.zh.md: f27a82459cdacdb4a2da27cf12b30c93f2dfdf64
|
2026-08-09-client-conversation-node-assembly.zh.md: 0e0fbdf8f3320393022528e6e3fe2cf0d492a1d3
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
+2
@@ -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。
|
新增业务节点可以局部注册自己的 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。
|
初始尾页、older prepend 和 live append 共享一套 Context 不变量。缺 start、Reader window gap、Location unknown 以及高频 delta 都是引擎明确表达的状态,不需要业务另建方向相关 cache。
|
||||||
|
|
||||||
Append 不扫描历史 Context;prepend 只 replay Match、Location 或 Reader 答案真正受影响的 Context。Chat 结构变化仍可能重算 visible order 和索引,但不会重跑无关业务 fold 或替换未变化 Node identity。
|
Append 不扫描历史 Context;prepend 只 replay Match、Location 或 Reader 答案真正受影响的 Context。Chat 结构变化仍可能重算 visible order 和索引,但不会重跑无关业务 fold 或替换未变化 Node identity。
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/subsystems/compaction.md
|
# pnpm run verify-translation-pairing --write docs/subsystems/compaction.md
|
||||||
compaction.md: 1aca48d677a83a488d9590edad3f8a68ca662b70
|
compaction.md: 11ee6eae797db1c57bf2ffbb90f1f37f47b8400c
|
||||||
compaction.zh.md: e2cfabc0891126d9f2165e47dffcf485f4a8a339
|
compaction.zh.md: a560e47dbb7aa92f0757179534803bbcaaebc6ba
|
||||||
|
|||||||
@@ -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.
|
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`
|
## `CompactionResult`
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。
|
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 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`
|
## `CompactionResult`
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ type TurnTailNodeViewProps = ChatNodeViewProps<'turn-tail'> & PropsRenderSlots<'
|
|||||||
|
|
||||||
/** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */
|
/** Turn-local actions and feature tail over the Location index, independent of Assistant placement. */
|
||||||
export const TurnTailNodeView = memo(function TurnTailNodeView({
|
export const TurnTailNodeView = memo(function TurnTailNodeView({
|
||||||
node, openFile, forkAt, renderSlotChain, t,
|
node, openFile, forkAt, renderSlotChain, t, useSession,
|
||||||
}: TurnTailNodeViewProps) {
|
}: TurnTailNodeViewProps) {
|
||||||
const data = node.data
|
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'
|
const turn = node.location.kind === 'turn' || node.location.kind === 'step'
|
||||||
? node.location.turn
|
? node.location.turn
|
||||||
: undefined
|
: undefined
|
||||||
@@ -34,7 +36,7 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
|
|||||||
tokensPerSecond={data.tokensPerSecond}
|
tokensPerSecond={data.tokensPerSecond}
|
||||||
clock="end"
|
clock="end"
|
||||||
onBranch={() => { forkAt(closing.finalNode.seq) }}
|
onBranch={() => { forkAt(closing.finalNode.seq) }}
|
||||||
branchUnavailable={data.branchUnavailable}
|
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
|
||||||
className={css.actions}
|
className={css.actions}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export interface TurnTailChatData {
|
|||||||
readonly time: number
|
readonly time: number
|
||||||
/** Last finalized content-bearing Assistant in this Turn. */
|
/** Last finalized content-bearing Assistant in this Turn. */
|
||||||
readonly closing: FinalAssistantChatData | null
|
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 branchUnavailable: boolean
|
||||||
readonly ttftMs?: number
|
readonly ttftMs?: number
|
||||||
readonly tokensPerSecond?: number
|
readonly tokensPerSecond?: number
|
||||||
|
|||||||
@@ -181,14 +181,12 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
|
|||||||
if (key !== 'conversation.chat.node') return opts?.fallback ?? null
|
if (key !== 'conversation.chat.node') return opts?.fallback ?? null
|
||||||
const nodeOwner = owner as RoutedChatNodeOwner
|
const nodeOwner = owner as RoutedChatNodeOwner
|
||||||
const nodeKey = opts?.hookContext as string | undefined
|
const nodeKey = opts?.hookContext as string | undefined
|
||||||
const useTurnData = ((dataKey: string) => {
|
const useTurnData: UseChatNodeTurnData = dataKey => props.useSession((snapshot) => {
|
||||||
return props.useSession((snapshot) => {
|
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
|
||||||
const location = nodeKey === undefined ? undefined : snapshot.chat.nodes.get(nodeKey)?.location
|
return location?.kind === 'turn' || location?.kind === 'step'
|
||||||
return location?.kind === 'turn' || location?.kind === 'step'
|
? location.turn.data.get(dataKey)
|
||||||
? location.turn.data.get(dataKey as never)
|
: undefined
|
||||||
: undefined
|
})
|
||||||
})
|
|
||||||
}) as UseChatNodeTurnData
|
|
||||||
const nodeProps = <Kind extends ChatNode['kind']>(): ChatNodeViewProps<Kind> => (
|
const nodeProps = <Kind extends ChatNode['kind']>(): ChatNodeViewProps<Kind> => (
|
||||||
{ ...props, ...nodeOwner, useTurnData } as unknown as ChatNodeViewProps<Kind>
|
{ ...props, ...nodeOwner, useTurnData } as unknown as ChatNodeViewProps<Kind>
|
||||||
)
|
)
|
||||||
@@ -722,6 +720,28 @@ describe('ChatView', () => {
|
|||||||
expect(h.forkAt.mock.calls).toEqual([[2]])
|
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', () => {
|
it('keeps final content actions but disables branch when Tool and interrupted Think follow it', () => {
|
||||||
const interruptedThink: AssistantMessageNode = {
|
const interruptedThink: AssistantMessageNode = {
|
||||||
kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2,
|
kind: 'assistant', seq: 4.1, time: 4_100, turn: 1, step: 2,
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ interface DeliverablesState extends DeliverablesTurnData {
|
|||||||
* Paths a call view reports having created or changed, by render intent rather
|
* 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
|
* 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
|
* 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[] {
|
function producedPaths(view: ToolResultNode['callView']): readonly string[] {
|
||||||
if (view === null) return []
|
if (view === null) return []
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ export default defineConfig({
|
|||||||
'packages/client/ui-workspace/src/client/index.ts',
|
'packages/client/ui-workspace/src/client/index.ts',
|
||||||
'packages/client/test-runtime/src/translate.ts',
|
'packages/client/test-runtime/src/translate.ts',
|
||||||
'packages/client/ui-primitives/src/JsonTree.tsx',
|
'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
|
// Typert generator: correctness is pinned by its fixture suites and
|
||||||
// the byte-for-byte catalog reproduction test; per-file coverage
|
// the byte-for-byte catalog reproduction test; per-file coverage
|
||||||
// would put whole-workspace compiler analysis under v8
|
// would put whole-workspace compiler analysis under v8
|
||||||
|
|||||||
Reference in New Issue
Block a user