diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index a1db84f50c..ca7a96027a 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -492,6 +492,21 @@ function requestKey(turn: number | null, group: string): string { return `${turn}\u0000${group}` } +function indexRequestBoundaries(records: readonly TableRecord[]): ReadonlyMap { + const boundaries = new Map() + 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, ): ReadonlyMap { const numbers = new Map() 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 diff --git a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx index 2b2e50fc65..2e38aee078 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryView.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryView.tsx @@ -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) diff --git a/packages/client/ui-trajectory/src/client/layout.ts b/packages/client/ui-trajectory/src/client/layout.ts index 6a24967ac2..265d5ba034 100644 --- a/packages/client/ui-trajectory/src/client/layout.ts +++ b/packages/client/ui-trajectory/src/client/layout.ts @@ -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 partial: ConversationSnapshot['partial'] runningCalls: ConversationSnapshot['runningCalls'] requests?: readonly RequestView[] @@ -71,7 +73,7 @@ type CompactionRequestView = Extract 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(resultByCall) for (const call of runningCalls) callById.set(call.callId, call) const emittedCallIds = indexAssistantCallIds(nodes) + const followingAssistants = indexFollowingAssistants(nodes) const callStartById = new Map() 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() 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(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, diff --git a/packages/client/ui-trajectory/src/client/trajectory-contract.ts b/packages/client/ui-trajectory/src/client/trajectory-contract.ts index 3e877a7969..7f1c1feea9 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-contract.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-contract.ts @@ -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 readonly requests: readonly RequestView[] readonly callSchemas: ReadonlyMap readonly partial: PartialAssistant | null diff --git a/packages/client/ui-trajectory/src/client/trajectory-definition-common.ts b/packages/client/ui-trajectory/src/client/trajectory-definition-common.ts index 639b1ad3ea..d55d5ca542 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-definition-common.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-definition-common.ts @@ -22,6 +22,7 @@ export function trajectoryNode( id: context.id, target: 'trajectory', anchorSeq, + location: context.start?.location ?? { kind: 'unresolved' }, data, } } diff --git a/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts b/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts index dcc5f2edbc..8ca382bcc5 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-snapshot-builder.ts @@ -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() 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, diff --git a/packages/client/ui-trajectory/tests/conversation-definitions.spec.ts b/packages/client/ui-trajectory/tests/conversation-definitions.spec.ts index 631d283f21..9999169896 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.spec.ts @@ -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']) diff --git a/packages/client/ui-trajectory/tests/layout.spec.tsx b/packages/client/ui-trajectory/tests/layout.spec.tsx index d04c95b46d..ec8924505f 100644 --- a/packages/client/ui-trajectory/tests/layout.spec.tsx +++ b/packages/client/ui-trajectory/tests/layout.spec.tsx @@ -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([[ + 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([[ + 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 }, diff --git a/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts b/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts index d3cf64ac2d..c0058b75c6 100644 --- a/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts +++ b/packages/client/ui-trajectory/tests/snapshot-builder.spec.ts @@ -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 }, })), ] diff --git a/packages/client/ui-trajectory/tests/table.spec.tsx b/packages/client/ui-trajectory/tests/table.spec.tsx index dc4d2c9188..b610b278f6 100644 --- a/packages/client/ui-trajectory/tests/table.spec.tsx +++ b/packages/client/ui-trajectory/tests/table.spec.tsx @@ -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() + + 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() const tablePane = screen.getByRole('table').parentElement as HTMLElement diff --git a/packages/client/ui-trajectory/tests/views.spec.tsx b/packages/client/ui-trajectory/tests/views.spec.tsx index 88c7f41ccf..e4ea44d925 100644 --- a/packages/client/ui-trajectory/tests/views.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.spec.tsx @@ -75,6 +75,7 @@ function historySnapshot( ): ConversationSnapshot { const trajectory: TrajectorySnapshot = { eventNodes: nodes, + eventLocations: new Map(), requests: [], callSchemas: new Map(), partial: null,