fix(client): review fixes for invocation rendering and turn boundaries

The user-turn predicate (opensUserTurn) unifies the three parallel
consumers a new node kind silently missed — produced-files turn reset,
retry liveness, own-words force-scroll — so a skill invocation behaves as
the turn opener it is. The menu marker resolves through ctx.locale.bind
instead of a hand-rolled snapshot lookup; the dead legacy <skill> render
arm goes with the removal cut; command-over-skill name precedence is now
documented at the matchEnter seam; and the emptied replacement catalog
keeps the no-reload sentence, with the never-published residual recorded
in the Agent Note.
This commit is contained in:
Yichen Jiang
2026-08-08 11:30:16 +08:00
parent c4c2355b50
commit 31ed85900d
19 changed files with 77 additions and 31 deletions
@@ -3,6 +3,7 @@
* nodes. Client-only and model-free: the vocabulary is the mutation tools'
* own follow-along `locations`, never the closing prose.
*/
import { opensUserTurn } from '@deepseek-ai/dsh-client-runtime/client'
import type { ConversationNode, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -62,7 +63,7 @@ export function producedForClosing(nodes: readonly ConversationNode[], seq: numb
}
continue
}
if (node.kind === 'user') {
if (opensUserTurn(node)) {
turn = undefined
pending = []
seen = new Set()
@@ -73,6 +73,27 @@ describe('producedForClosing derivation', () => {
expect(producedForClosing(nodes, 999)).toEqual([])
})
it('treats a user-explicit skill invocation as a turn boundary', () => {
// The injection opens a user turn exactly like a typed prompt: files
// written before it must not spill into the turn its answer closes.
const skillInvocation = {
kind: 'skill-invocation' as const, seq: 4, time: 4_000,
name: 'hidden-demo',
content: [{ type: 'text', text: '<skill_content name="hidden-demo">x</skill_content>' }] as never,
source: null,
}
const nodes: ConversationNode[] = [
user(1, 'write things'),
assistant(2, 'wrote', 1),
wrote(3, 'a', 'stale.txt'),
skillInvocation,
wrote(5, 'b', 'fresh.txt'),
assistant(6, 'followed the skill', 2),
]
expect(producedForClosing(nodes, 6)).toEqual(['fresh.txt'])
expect(producedForClosing(nodes, 6)).not.toContain('stale.txt')
})
it('counts a generic edit and never spills across the turn boundary', () => {
const inserted = (seq: number, callId: string, path: string): ToolResultNode => ({
...toolResult(seq, callId, 'str_replace_editor'),