fix(ui-trajectory): place steering before request boundary
This commit is contained in:
@@ -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'])
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
})),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -75,6 +75,7 @@ function historySnapshot(
|
||||
): ConversationSnapshot {
|
||||
const trajectory: TrajectorySnapshot = {
|
||||
eventNodes: nodes,
|
||||
eventLocations: new Map(),
|
||||
requests: [],
|
||||
callSchemas: new Map(),
|
||||
partial: null,
|
||||
|
||||
Reference in New Issue
Block a user