fix(ui-trajectory): place steering before request boundary

This commit is contained in:
imccyu
2026-08-11 01:06:11 +08:00
parent 98a4cc6569
commit d66e693c63
11 changed files with 303 additions and 47 deletions

View File

@@ -228,21 +228,9 @@ describe('Trajectory conversation Definitions', () => {
})
it('classifies claimed inbox input as steering and consumes one inherited prompt change', () => {
const current = snapshot(assembler([
at(1, 'agent/inbox/spliced', {
target: 'next-step', start: 0, removedCount: 0, inserted: [{ id: 'm1' }],
}),
at(2, 'agent/inbox/spliced', {
target: 'next-step', start: 0, removedCount: 1, inserted: [],
}),
at(3, 'user/message', {
id: 'm1',
role: 'user',
content: [{ type: 'text', text: 'steer here' }],
source: { kind: 'user' },
}),
at(4, 'turn/start', { turn: 1 }),
at(5, 'request/header', {
const value = assembler([
at(1, 'turn/start', { turn: 1 }),
at(2, 'request/header', {
reason: 'initial',
header: {
config: { provider: 'test', model: 'test' },
@@ -250,22 +238,45 @@ describe('Trajectory conversation Definitions', () => {
tools: [],
},
}),
at(6, 'step/start', { turn: 1, step: 1 }),
at(7, 'assistant/message', {
at(3, 'step/start', { turn: 1, step: 1 }),
at(4, 'assistant/message', {
turn: 1,
step: 1,
message: assistantMessage('assistant-1', 'first'),
}),
at(8, 'step/end', { turn: 1, step: 1 }),
at(9, 'step/start', { turn: 1, step: 2 }),
at(10, 'assistant/message', {
turn: 1,
step: 2,
message: assistantMessage('assistant-2', 'second'),
at(5, 'step/end', { turn: 1, step: 1 }),
at(6, 'agent/inbox/spliced', {
target: 'next-step', start: 0, removedCount: 0, inserted: [{ id: 'm1' }],
}),
]))
at(7, 'agent/inbox/spliced', {
target: 'next-step', start: 0, removedCount: 1, inserted: [],
}),
at(8, 'step/start', { turn: 1, step: 2 }),
])
value.append(at(9, 'user/message', {
id: 'm1',
role: 'user',
content: [{ type: 'text', text: 'steer here' }],
source: { kind: 'user' },
}))
value.flush()
const steering = snapshot(value)
expect(steering.eventNodes.find(node => node.seq === 9)?.kind).toBe('steering')
expect(steering.eventLocations.get(9)).toMatchObject({
kind: 'step',
turn: { turn: 1 },
step: { step: 2 },
})
value.append(at(10, 'assistant/message', {
turn: 1,
step: 2,
message: assistantMessage('assistant-2', 'second'),
}))
value.flush()
const current = snapshot(value)
expect(current.eventNodes.find(node => node.seq === 3)?.kind).toBe('steering')
expect(current.requests.map(request => request.purpose === 'assistant'
? request.prompt?.system
: undefined)).toEqual(['system prompt', 'system prompt'])

View File

@@ -6,7 +6,7 @@
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import type {
ConversationSnapshot, RequestView,
ConversationLocation, ConversationSnapshot, RequestView,
} from '@deepseek-ai/dsh-client-runtime/client'
import { TrajectoryGroupHeader } from '../src/client/TrajectoryGroupHeader.tsx'
import { TrajectoryTurn } from '../src/client/TrajectoryTurn.tsx'
@@ -239,6 +239,112 @@ describe('deriveTrajectoryLayout', () => {
])
})
it('places steering in its resolved step instead of the turn-opening Message group', () => {
const nodes = [
{ kind: 'user', seq: 1, time: 1_000, content: [{ type: 'text', text: 'start' }], source: null },
{
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1,
blocks: [{ kind: 'text', text: 'first step' }],
},
{
kind: 'steering', messageId: 'steer-1', seq: 3, time: 3_000,
content: [{ type: 'text', text: 'change direction' }], source: null,
},
{
kind: 'assistant', seq: 4, time: 4_000, turn: 1, step: 2,
blocks: [{ kind: 'text', text: 'second step' }],
},
] as unknown as ConversationSnapshot['nodes']
const data = { get: () => undefined }
const step = { turn: 1, step: 2, start: undefined, end: undefined, status: 'open' as const, data }
const turn = {
turn: 1, start: undefined, end: undefined, status: 'open' as const, steps: [step], data,
}
const eventLocations = new Map<number, ConversationLocation>([[
3,
{ kind: 'step', turn, step },
]])
const turns = deriveTrajectoryLayout({
nodes,
eventLocations,
partial: null,
runningCalls: [],
})
expect(turns).toHaveLength(1)
expect(turns[0]?.groups.map(group => group.title)).toEqual([
'Message', 'Step 1', 'Step 2',
])
expect(turns[0]?.groups[2]?.cells).toMatchObject([
{ kind: 'user', previewMarkdown: 'change direction', sourceSeq: 3 },
{ kind: 'message', previewMarkdown: 'second step', sourceSeq: 4 },
])
})
it('keeps a running request boundary after steering input', () => {
const nodes = [{
kind: 'steering', messageId: 'steer-1', seq: 3, time: 3_000,
content: [{ type: 'text', text: 'change direction' }], source: null,
}] as unknown as ConversationSnapshot['nodes']
const data = { get: () => undefined }
const step = { turn: 1, step: 2, start: undefined, end: undefined, status: 'open' as const, data }
const turn = {
turn: 1, start: undefined, end: undefined, status: 'open' as const, steps: [step], data,
}
const eventLocations = new Map<number, ConversationLocation>([[
3,
{ kind: 'step', turn, step },
]])
const turns = deriveTrajectoryLayout({
nodes,
eventLocations,
partial: null,
runningCalls: [],
requests: [{
purpose: 'assistant',
startSeq: 2,
turn: 1,
step: 2,
startedAt: 2_000,
completedAt: null,
status: 'running',
}],
})
expect(turns[0]?.groups[0]?.cells).toMatchObject([
{ kind: 'user', previewMarkdown: 'change direction', sourceSeq: 3 },
{ kind: 'message', requestOnly: true, sourceSeq: 2 },
])
})
it('uses the following assistant step while a historical window lacks steering Location', () => {
const nodes = [
{
kind: 'steering', messageId: 'steer-1', seq: 3, time: 3_000,
content: [{ type: 'text', text: 'change direction' }], source: null,
},
{
kind: 'assistant', seq: 4, time: 4_000, turn: 2, step: 3,
blocks: [{ kind: 'text', text: 'continued' }],
},
] as unknown as ConversationSnapshot['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
expect(turns[0]).toMatchObject({
turn: 2,
groups: [{
title: 'Step 3',
cells: [
{ kind: 'user', previewMarkdown: 'change direction' },
{ kind: 'message', previewMarkdown: 'continued' },
],
}],
})
})
it('places standalone compaction chronologically in its own between-turn section', () => {
const nodes = [
{ kind: 'user', seq: 1, time: 1_000, content: [{ type: 'text', text: 'first' }], source: null },

View File

@@ -22,7 +22,11 @@ function contribution(
anchorSeq: number,
data: TrajectoryContribution,
): TrajectoryConversationViewNode {
return { key, kind: key, id: key, target: 'trajectory', anchorSeq, data }
return {
key, kind: key, id: key, target: 'trajectory', anchorSeq,
location: { kind: 'session' },
data,
}
}
function stepLocation(turn: number, step: number): TrajectoryRequestHeaderState['location'] {
@@ -72,6 +76,7 @@ describe('TrajectorySnapshotBuilder', () => {
id: '2',
target: 'trajectory',
anchorSeq: 2,
location: { kind: 'session' },
data: {
kind: 'request-header',
header: {
@@ -89,6 +94,7 @@ describe('TrajectorySnapshotBuilder', () => {
id: `1:${request.step}`,
target: 'trajectory' as const,
anchorSeq: request.startSeq,
location: { kind: 'session' as const },
data: { kind: 'assistant' as const, partial: null, request },
})),
]

View File

@@ -308,6 +308,43 @@ describe('TrajectoryTable', () => {
expect(screen.getByText('Request #2')).toBeTruthy()
})
it('places the request boundary after leading steering input', () => {
const turns: readonly TrajectoryTurnModel[] = [{
turn: 1,
groups: [{
title: 'Step 2',
cells: [{
index: 1,
kind: 'user',
sourceSeq: 3,
text: 'change direction',
timeSeconds: 0,
}, {
index: 2,
kind: 'message',
sourceSeq: 4,
text: 'continued',
timeSeconds: 1,
}],
}],
}]
render(<TrajectoryTable
turns={turns}
requestNumbers={[{
seq: 2,
turn: 1,
step: 2,
group: 'Step 2',
number: 1,
}]}
{...FOLD_PROPS}
/>)
const request = screen.getByRole('button', { name: 'Request #1' })
expect(request.closest('tr')?.getAttribute('aria-label')).toContain('ASSISTANT')
})
it('follows appended records only while the ledger is already at the bottom', () => {
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
const tablePane = screen.getByRole('table').parentElement as HTMLElement

View File

@@ -75,6 +75,7 @@ function historySnapshot(
): ConversationSnapshot {
const trajectory: TrajectorySnapshot = {
eventNodes: nodes,
eventLocations: new Map(),
requests: [],
callSchemas: new Map(),
partial: null,