fix(client): address conversation assembly review
This commit is contained in:
@@ -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-deliverables/README.md
|
||||
README.md: 189dedd88fed6914012204118ccdf9bdd0cd3bb2
|
||||
README.zh.md: ba493549bd0bc3f8a2adbda5989448e497f0af93
|
||||
README.md: 7d03e5faedda3ba8c9cc4cab6ca134d98dc7ec13
|
||||
README.zh.md: dfbbc7a39aa94aab438119a4f23ffb02da2daa3d
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
Produced-files feature owner: registers the deliverables row a finished turn ends with into the chat view's `conversation.chat.turnTail` hole. All policy lives here; removing this plugin's line from cordis.yml removes the surface entirely, and the owning view renders an empty hole at zero cost.
|
||||
|
||||
`producedForClosing` derives one turn's produced files from the tail hole's owner currency — the finalized snapshot nodes and the closing assistant's seq. The vocabulary is the mutation tools' own follow-along `locations`, never the closing prose: a produced file is listed whether or not the model remembered to name it. A mutation is recognized by render intent, not tool name — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a new mutation tool joins by declaring what it does. Reads, deletes, and failed calls contribute nothing; a path appears once per turn in first-seen order; accumulation resets on the turn boundary, so a turn that mutates and then ends without content text cannot spill into the next turn's row.
|
||||
`deliverablesDefinition` folds each Turn's successful mutation calls into engine-published `DeliverablesTurnData`; `producedForClosing` reads that data with the closing Assistant seq. The vocabulary is the mutation tools' own follow-along `locations`, never the closing prose: a produced file is listed whether or not the model remembered to name it. A mutation is recognized by render intent, not tool name — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a new mutation tool joins by declaring what it does. Reads, deletes, and failed calls contribute nothing; a path appears once per Turn in first-seen order. The Conversation Location index owns Turn membership, so a Turn that mutates and then ends without content text cannot spill into the next Turn's row.
|
||||
|
||||
`ProducedFiles` renders the row between the closing message's body and its IconActions footer: a quiet label, up to six chips (basename text, full path as the `title`), and an explicit remainder count past the cap. Each chip opens through the owner-supplied `openFile` — the same Host opener the tool rows use, with the chat view resolving relative paths against the session cwd. Design rationale: the [workspace file links Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
产出文件功能的属主:把已完成轮次末尾的产出文件行注册到 chat 视图的 `conversation.chat.turnTail` slot 中。全部策略都在本包内;从 cordis.yml 中删去本插件那一行即可整体移除该界面,属主视图无需额外开销即可渲染空 slot。
|
||||
|
||||
`producedForClosing` 根据 tail slot 属主提供的当前数据,即定稿快照节点和收尾助手的 seq,推导一个轮次产出的文件。依据的是修改工具自身附带的 `locations`,而不是收尾正文:无论模型是否记得点名,产出文件都会被列出。修改操作按渲染意图而非工具名识别:diff 卡片,或 `kind` 为 `edit` 的通用卡片(即 `str_replace_editor` 的 insert 操作所呈现的形态);因此新的修改工具只需声明自身行为即可加入。读取、删除和失败的调用不贡献任何条目;同一路径在一轮内按首见顺序只出现一次;累积在轮次边界重置,因此一轮若先改写文件、随后没有正文内容就结束,不会溢进下一轮的行里。
|
||||
`deliverablesDefinition` 把每个 Turn 中成功的修改调用折叠进引擎发布的 `DeliverablesTurnData`;`producedForClosing` 结合收尾 Assistant 的 seq 读取这份数据。依据的是修改工具自身附带的 `locations`,而不是收尾正文:无论模型是否记得点名,产出文件都会被列出。修改操作按渲染意图而非工具名识别:diff 卡片,或 `kind` 为 `edit` 的通用卡片(即 `str_replace_editor` 的 insert 操作所呈现的形态);因此新的修改工具只需声明自身行为即可加入。读取、删除和失败的调用不贡献任何条目;同一路径在一个 Turn 内按首见顺序只出现一次。Conversation Location 索引拥有 Turn 成员关系,因此一个 Turn 即使先修改文件、随后没有正文内容就结束,也不会溢进下一个 Turn 的行里。
|
||||
|
||||
`ProducedFiles` 在收尾消息正文与其 IconActions 之间渲染该行:一个低调的标签、至多六个标签项(文本为文件名,完整路径作为 `title`),超出上限则显示一个明确的剩余计数。每个标签项经由属主提供的 `openFile` 打开——与工具行相同的 Host 打开器,chat 视图会把相对路径按会话 cwd 解析。设计原理:[workspace 文件链接 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.md)。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* ui-deliverables browser half: the derivation contract of
|
||||
* `producedForClosing` over finalized snapshot nodes, the row's rendering
|
||||
* `producedForClosing` over engine-published Turn data, the row's rendering
|
||||
* and opener wiring, and the plugin registrations' fiber-teardown removal
|
||||
* (HMR safety) against the real SlotsService.
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ConversationEventRegistry, ConversationNodeAssembler, SlotsService,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ConversationEventInput, ConversationLocationDataStore, ConversationNodeDefinition,
|
||||
ConversationEventInput, ConversationLocationDataStore, ConversationMatch, ConversationNodeDefinition,
|
||||
ConversationTimelineSnapshot, ConversationTurnDataMap, ConversationViewDefinition,
|
||||
ConversationViewNode, ToolResultNode, TurnLocation,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -107,6 +107,10 @@ function at(
|
||||
}
|
||||
}
|
||||
|
||||
function matched(input: ConversationEventInput, role: ConversationMatch['role']): ConversationMatch {
|
||||
return { ...input, role, location: { kind: 'unresolved' } }
|
||||
}
|
||||
|
||||
function call(
|
||||
seq: number,
|
||||
callId: string,
|
||||
@@ -190,6 +194,50 @@ describe('produced-file Turn data', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('ignores calls without mutation locations, orphan results, and replacement results', () => {
|
||||
const replacement = result(8, 'replacement')
|
||||
const value = assembler([
|
||||
at(1, 'turn/start', { turn: 1 }),
|
||||
at(2, 'tool/call', { turn: 1, step: 1, callId: 'no-view', name: 'fixture', arguments: '{}' }),
|
||||
result(3, 'no-view'),
|
||||
call(4, 'locationless-edit', { card: 'generic', title: 'Edit', kind: 'edit' }),
|
||||
result(5, 'locationless-edit'),
|
||||
result(6, 'orphan'),
|
||||
call(7, 'replacement', diff('replaced.txt')),
|
||||
{
|
||||
...replacement,
|
||||
event: {
|
||||
...replacement.event,
|
||||
surfaceOp: { op: 'replace', start: 1, end: 1 },
|
||||
} as ConversationEventInput['event'],
|
||||
},
|
||||
at(9, 'turn/end', { turn: 1, reason: { kind: 'completed' } }),
|
||||
])
|
||||
|
||||
expect(producedForClosing(deliverablesOf(value))).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an invalid start match and preserves state for an unrelated update', () => {
|
||||
const startMatch = matched(at(1, 'turn/start', { turn: 1 }), 'start')
|
||||
const emptyContext: Parameters<typeof deliverablesDefinition.start>[0] = {
|
||||
key: 'deliverables:1',
|
||||
kind: 'deliverables',
|
||||
id: '1',
|
||||
matches: [startMatch],
|
||||
start: startMatch,
|
||||
state: undefined,
|
||||
current: new Map(),
|
||||
}
|
||||
const reader: Parameters<typeof deliverablesDefinition.start>[2] = { previous: () => undefined }
|
||||
const state = deliverablesDefinition.start(emptyContext, startMatch, reader)
|
||||
const unrelated = matched(at(2, 'turn/end', { turn: 1, reason: { kind: 'completed' } }), 'update')
|
||||
const context: Parameters<typeof deliverablesDefinition.update>[0] = { ...emptyContext, state }
|
||||
|
||||
expect(() => deliverablesDefinition.start(emptyContext, unrelated, reader))
|
||||
.toThrow('deliverables start requires turn/start')
|
||||
expect(deliverablesDefinition.update(context, unrelated)).toBe(state)
|
||||
})
|
||||
|
||||
it('replays a tail page once prepend supplies its missing Turn start', () => {
|
||||
const value = assembler([
|
||||
call(10, 'late', diff('history.txt')),
|
||||
|
||||
Reference in New Issue
Block a user