fix(ui-trajectory): place steering before request boundary
This commit is contained in:
@@ -492,6 +492,21 @@ function requestKey(turn: number | null, group: string): string {
|
||||
return `${turn}\u0000${group}`
|
||||
}
|
||||
|
||||
function indexRequestBoundaries(records: readonly TableRecord[]): ReadonlyMap<string, number> {
|
||||
const boundaries = new Map<string, number>()
|
||||
for (const record of records) {
|
||||
const key = requestKey(record.turn, record.group)
|
||||
if (boundaries.has(key)) continue
|
||||
if (requestStep(record.group) === undefined) {
|
||||
if (record.groupStart) boundaries.set(key, record.cell.index)
|
||||
continue
|
||||
}
|
||||
if (record.cell.kind === 'user' || record.cell.kind === 'context') continue
|
||||
boundaries.set(key, record.cell.index)
|
||||
}
|
||||
return boundaries
|
||||
}
|
||||
|
||||
function sectionLabel(turn: number | null): string {
|
||||
return turn === null ? 'Between turns' : `Turn ${turn}`
|
||||
}
|
||||
@@ -499,16 +514,18 @@ function sectionLabel(turn: number | null): string {
|
||||
function indexRequestNumbers(
|
||||
records: readonly TableRecord[],
|
||||
sessionNumbers: readonly TrajectoryRequestNumber[] | undefined,
|
||||
boundaries: ReadonlyMap<string, number>,
|
||||
): ReadonlyMap<string, number> {
|
||||
const numbers = new Map<string, number>()
|
||||
for (const request of sessionNumbers ?? []) {
|
||||
numbers.set(requestKey(request.turn, request.group), request.number)
|
||||
}
|
||||
let next = Math.max(0, ...numbers.values()) + 1
|
||||
const boundaries = records
|
||||
.filter(record => record.groupStart && requestStep(record.group) !== undefined)
|
||||
const boundaryRecords = records
|
||||
.filter(record => boundaries.get(requestKey(record.turn, record.group)) === record.cell.index
|
||||
&& requestStep(record.group) !== undefined)
|
||||
.sort((left, right) => left.cell.index - right.cell.index)
|
||||
for (const record of boundaries) {
|
||||
for (const record of boundaryRecords) {
|
||||
const key = requestKey(record.turn, record.group)
|
||||
if (!numbers.has(key)) numbers.set(key, next++)
|
||||
}
|
||||
@@ -1731,9 +1748,10 @@ export function TrajectoryTable({
|
||||
useEffect(() => {
|
||||
onSelectedIndexChange?.(selectedIndex)
|
||||
}, [onSelectedIndexChange, selectedIndex])
|
||||
const requestBoundaries = useMemo(() => indexRequestBoundaries(allRecords), [allRecords])
|
||||
const requestNumbers = useMemo(
|
||||
() => indexRequestNumbers(allRecords, sessionRequestNumbers),
|
||||
[allRecords, sessionRequestNumbers],
|
||||
() => indexRequestNumbers(allRecords, sessionRequestNumbers, requestBoundaries),
|
||||
[allRecords, requestBoundaries, sessionRequestNumbers],
|
||||
)
|
||||
const records = useMemo(() => {
|
||||
if (searchMatchIndexes !== null) return filterRecords(allRecords, searchMatchIndexes)
|
||||
@@ -2218,10 +2236,11 @@ export function TrajectoryTable({
|
||||
const isRequestOnly = record.cell.requestOnly === true
|
||||
const isInitialSystem = record.cell.kind === 'system'
|
||||
&& record.cell.index === allRecords[0]?.cell.index
|
||||
const request = record.groupStart
|
||||
const key = requestKey(record.turn, record.group)
|
||||
const request = requestBoundaries.get(key) === record.cell.index
|
||||
&& !isCollapsedSummary
|
||||
&& (record.turn === null || !collapsedTurns.has(record.turn))
|
||||
? requestNumbers.get(requestKey(record.turn, record.group))
|
||||
? requestNumbers.get(key)
|
||||
: undefined
|
||||
const requestInfo = request === undefined
|
||||
? undefined
|
||||
|
||||
@@ -145,6 +145,7 @@ export function TrajectoryView({
|
||||
snapshot.openState === 'loading' || snapshot.loadingOlder)
|
||||
const hasOlderHistory = useSession(snapshot => snapshot.hasMore)
|
||||
const nodes = inspection.eventNodes
|
||||
const eventLocations = inspection.eventLocations
|
||||
const historyBaseSeq = nodes[0]?.seq ?? 0
|
||||
const partial = inspection.partial
|
||||
const runningCalls = inspection.runningCalls
|
||||
@@ -254,6 +255,7 @@ export function TrajectoryView({
|
||||
const finalized = useMemo(() => {
|
||||
const turns = deriveTrajectoryLayout({
|
||||
nodes,
|
||||
eventLocations,
|
||||
partial: partialTurn === null || partialStep === null
|
||||
? null
|
||||
: { turn: partialTurn, step: partialStep, blocks: [] },
|
||||
@@ -263,7 +265,7 @@ export function TrajectoryView({
|
||||
})
|
||||
return { turns, lastIndex: lastCellIndex(turns) }
|
||||
}, [
|
||||
nodes, partialTurn, partialStep,
|
||||
nodes, eventLocations, partialTurn, partialStep,
|
||||
runningCalls, requests, callSchemas,
|
||||
])
|
||||
const timelinePartialSignature = partialStructureSignature(partial)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type {
|
||||
AssistantBlock,
|
||||
AssistantMessageNode,
|
||||
ConversationLocation,
|
||||
ConversationSnapshot,
|
||||
RequestInspectionSnapshot,
|
||||
RequestPromptChange,
|
||||
@@ -34,6 +35,7 @@ export interface TrajectoryTurnModel {
|
||||
/** Snapshot slice the trajectory view folds. */
|
||||
export interface TrajectoryLayoutInput {
|
||||
nodes: ConversationSnapshot['nodes']
|
||||
eventLocations?: ReadonlyMap<number, ConversationLocation>
|
||||
partial: ConversationSnapshot['partial']
|
||||
runningCalls: ConversationSnapshot['runningCalls']
|
||||
requests?: readonly RequestView[]
|
||||
@@ -71,7 +73,7 @@ type CompactionRequestView = Extract<RequestView, { purpose: 'compaction' }>
|
||||
|
||||
type InputNode = Extract<
|
||||
ConversationSnapshot['nodes'][number],
|
||||
{ kind: 'user' | 'context' }
|
||||
{ kind: 'user' | 'steering' | 'context' }
|
||||
>
|
||||
|
||||
type OrderedLayoutEntry =
|
||||
@@ -135,12 +137,13 @@ function inputCellDetail(node: InputNode): Pick<
|
||||
*/
|
||||
export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly TrajectoryTurnModel[] {
|
||||
const {
|
||||
nodes, partial, runningCalls, requests = [], callSchemas,
|
||||
nodes, eventLocations, partial, runningCalls, requests = [], callSchemas,
|
||||
} = input
|
||||
const resultByCall = indexResults(nodes)
|
||||
const callById = new Map<string, ToolCallBlock>(resultByCall)
|
||||
for (const call of runningCalls) callById.set(call.callId, call)
|
||||
const emittedCallIds = indexAssistantCallIds(nodes)
|
||||
const followingAssistants = indexFollowingAssistants(nodes)
|
||||
const callStartById = new Map<string, number>()
|
||||
for (const result of resultByCall.values()) {
|
||||
const startedAt = finiteTime(result.callTime)
|
||||
@@ -185,6 +188,19 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
}
|
||||
groups.push({ title, laid: [...laid] })
|
||||
}
|
||||
const pushStepInput = (turn: number, step: number, laid: readonly LaidCell[]) => {
|
||||
if (laid.length === 0) return
|
||||
const groups = bucket(turn).groups
|
||||
const title = `Step ${step}`
|
||||
const existing = groups.find(group => group.title === title)
|
||||
if (existing === undefined) {
|
||||
groups.push({ title, laid: [...laid] })
|
||||
return
|
||||
}
|
||||
const request = existing.laid.findIndex(entry => entry.cell.requestOnly === true)
|
||||
if (request === -1) existing.laid.push(...laid)
|
||||
else existing.laid.splice(request, 0, ...laid)
|
||||
}
|
||||
|
||||
const representedRequests = new Set<string>()
|
||||
for (const node of nodes) {
|
||||
@@ -338,7 +354,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
if (node.kind === 'user') {
|
||||
// user/message has no turn on the wire; enclose it in the next assistant
|
||||
// (or partial) turn, else open the turn after the last assistant.
|
||||
const turn = enclosingUserTurn(nodes, i, partial, lastAssistantTurn)
|
||||
const turn = enclosingUserTurn(followingAssistants[i], partial, lastAssistantTurn)
|
||||
pushMessage(turn, {
|
||||
absTime: finiteTime(node.time),
|
||||
cell: {
|
||||
@@ -351,6 +367,26 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
continue
|
||||
}
|
||||
if (node.kind === 'steering') {
|
||||
const placement = steeringPlacement(
|
||||
followingAssistants[i],
|
||||
partial,
|
||||
lastAssistantTurn,
|
||||
eventLocations?.get(node.seq),
|
||||
)
|
||||
const laid = {
|
||||
absTime: finiteTime(node.time),
|
||||
cell: {
|
||||
index: ++index,
|
||||
kind: 'user' as const,
|
||||
...inputCellDetail(node),
|
||||
},
|
||||
}
|
||||
if (placement.step === undefined) pushMessage(placement.turn, laid)
|
||||
else pushStepInput(placement.turn, placement.step, [laid])
|
||||
prevAbsTime = finiteTime(node.time) ?? prevAbsTime
|
||||
continue
|
||||
}
|
||||
if (node.kind === 'assistant') {
|
||||
const laidList = withSubCalls(
|
||||
expandAssistant(node, index + 1, prevAbsTime, resultByCall, callStartById, callById),
|
||||
@@ -364,7 +400,7 @@ export function deriveTrajectoryLayout(input: TrajectoryLayoutInput): readonly T
|
||||
continue
|
||||
}
|
||||
if (node.kind === 'context') {
|
||||
const turn = enclosingUserTurn(nodes, i, partial, lastAssistantTurn)
|
||||
const turn = enclosingUserTurn(followingAssistants[i], partial, lastAssistantTurn)
|
||||
pushMessage(turn, {
|
||||
absTime: finiteTime(node.time),
|
||||
cell: {
|
||||
@@ -821,22 +857,53 @@ function stringifySourceValue(value: unknown): string {
|
||||
* in-flight partial, else the turn after the last finalized assistant (or 1).
|
||||
*/
|
||||
function enclosingUserTurn(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
userIndex: number,
|
||||
followingAssistant: AssistantMessageNode | undefined,
|
||||
partial: ConversationSnapshot['partial'],
|
||||
lastAssistantTurn: number | null,
|
||||
): number {
|
||||
for (let i = userIndex + 1; i < nodes.length; i++) {
|
||||
const n = nodes[i]
|
||||
/* v8 ignore next -- dense-array guard: i stays within nodes.length, so the undefined arm needs a sparse array no caller builds. */
|
||||
if (n === undefined) continue
|
||||
if (n.kind === 'assistant') return n.turn
|
||||
}
|
||||
if (followingAssistant !== undefined) return followingAssistant.turn
|
||||
if (partial !== null) return partial.turn
|
||||
if (lastAssistantTurn !== null) return lastAssistantTurn + 1
|
||||
return 1
|
||||
}
|
||||
|
||||
function steeringPlacement(
|
||||
followingAssistant: AssistantMessageNode | undefined,
|
||||
partial: ConversationSnapshot['partial'],
|
||||
lastAssistantTurn: number | null,
|
||||
location: ConversationLocation | undefined,
|
||||
): { turn: number; step?: number } {
|
||||
if (location?.kind === 'step') {
|
||||
return { turn: location.turn.turn, step: location.step.step }
|
||||
}
|
||||
const locatedTurn = location?.kind === 'turn' ? location.turn.turn : undefined
|
||||
if (followingAssistant !== undefined
|
||||
&& (locatedTurn === undefined || followingAssistant.turn === locatedTurn)) {
|
||||
return {
|
||||
turn: followingAssistant.turn,
|
||||
...(followingAssistant.step > 0 ? { step: followingAssistant.step } : {}),
|
||||
}
|
||||
}
|
||||
if (partial !== null && (locatedTurn === undefined || partial.turn === locatedTurn)) {
|
||||
return { turn: partial.turn, ...(partial.step > 0 ? { step: partial.step } : {}) }
|
||||
}
|
||||
if (locatedTurn !== undefined) return { turn: locatedTurn }
|
||||
return { turn: lastAssistantTurn ?? 1 }
|
||||
}
|
||||
|
||||
function indexFollowingAssistants(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
): readonly (AssistantMessageNode | undefined)[] {
|
||||
const following = new Array<AssistantMessageNode | undefined>(nodes.length)
|
||||
let assistant: AssistantMessageNode | undefined
|
||||
for (let index = nodes.length - 1; index >= 0; index--) {
|
||||
following[index] = assistant
|
||||
const node = nodes[index]
|
||||
if (node?.kind === 'assistant') assistant = node
|
||||
}
|
||||
return following
|
||||
}
|
||||
|
||||
function enclosingPromptTurn(
|
||||
nodes: ConversationSnapshot['nodes'],
|
||||
seq: number,
|
||||
|
||||
@@ -53,12 +53,14 @@ export type TrajectoryContribution =
|
||||
export interface TrajectoryConversationViewNode extends ConversationViewNode {
|
||||
readonly target: 'trajectory'
|
||||
readonly anchorSeq: number
|
||||
readonly location: ConversationLocation
|
||||
readonly data: TrajectoryContribution
|
||||
}
|
||||
|
||||
/** Stage-oriented Trajectory data assembled from registered business Contexts. */
|
||||
export interface TrajectorySnapshot {
|
||||
readonly eventNodes: readonly ConversationNode[]
|
||||
readonly eventLocations: ReadonlyMap<number, ConversationLocation>
|
||||
readonly requests: readonly RequestView[]
|
||||
readonly callSchemas: ReadonlyMap<string, ConversationPromptSnapshot['tools'][number]>
|
||||
readonly partial: PartialAssistant | null
|
||||
|
||||
@@ -22,6 +22,7 @@ export function trajectoryNode(
|
||||
id: context.id,
|
||||
target: 'trajectory',
|
||||
anchorSeq,
|
||||
location: context.start?.location ?? { kind: 'unresolved' },
|
||||
data,
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type ToolSchema = ConversationPromptSnapshot['tools'][number]
|
||||
/** Stable empty target used until a Session has assembled Trajectory records. */
|
||||
export const EMPTY_TRAJECTORY_SNAPSHOT: TrajectorySnapshot = {
|
||||
eventNodes: EMPTY_LIST,
|
||||
eventLocations: new Map(),
|
||||
requests: EMPTY_LIST,
|
||||
callSchemas: new Map(),
|
||||
partial: null,
|
||||
@@ -179,6 +180,7 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
if (key !== undefined) headersByStep.set(key, contribution.data.header)
|
||||
}
|
||||
const finalized: ConversationNode[] = []
|
||||
const eventLocations = new Map<number, TrajectoryConversationViewNode['location']>()
|
||||
const requests: RequestView[] = []
|
||||
const boundaries: { seq: number; time: number }[] = []
|
||||
const turnEndings: { turn: number; time: number; error?: string }[] = []
|
||||
@@ -198,6 +200,7 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
}
|
||||
if (data.kind === 'node') {
|
||||
finalized.push(data.node)
|
||||
eventLocations.set(data.node.seq, contribution.location)
|
||||
continue
|
||||
}
|
||||
if (data.kind === 'assistant') {
|
||||
@@ -244,6 +247,7 @@ export class TrajectorySnapshotBuilder implements ConversationViewBuilder<
|
||||
const eventNodes = finalized
|
||||
return {
|
||||
eventNodes,
|
||||
eventLocations,
|
||||
requests,
|
||||
callSchemas,
|
||||
partial,
|
||||
|
||||
@@ -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