feat(trajectory): implement trajectory step cell and layout with bilingual support
- Added TrajectoryCell, TrajectoryGroupHeader, and TrajectoryTurn components for rendering trajectory steps and groups. - Introduced bilingual support with English and Chinese translations for trajectory notes. - Updated conversation session models to include timestamps for various message types. - Enhanced layout logic to handle expanded assistant blocks and tool results with duration metrics. - Added CSS styles for new components to ensure proper display and alignment.
This commit is contained in:
@@ -20,7 +20,7 @@ afterEach(cleanup)
|
||||
const SID = 's1' as SessionId
|
||||
|
||||
const assistant = (seq: number, turn: number, usage?: unknown): AssistantMessageNode => ({
|
||||
kind: 'assistant', seq, turn, step: seq, blocks: [{ kind: 'text', text: `t${seq}` }],
|
||||
kind: 'assistant', seq, time: seq * 1_000, turn, step: seq, blocks: [{ kind: 'text', text: `t${seq}` }],
|
||||
...(usage === undefined ? {} : { usage }),
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('deriveStats', () => {
|
||||
|
||||
it('cache hit stays null with no cache accounting; non-assistant nodes ignored', () => {
|
||||
const tool: ToolResultNode = {
|
||||
kind: 'tool-result', seq: 5, callId: 'c', call: null, content: [],
|
||||
kind: 'tool-result', seq: 5, time: 5_000, callId: 'c', call: null, callTime: null, content: [],
|
||||
isError: false, callView: null, resultView: null,
|
||||
}
|
||||
const stats = deriveStats([tool, assistant(1, 1)])
|
||||
@@ -112,8 +112,9 @@ describe('bash sample row', () => {
|
||||
const CHILD = 'child-1' as SessionId
|
||||
|
||||
const result = (callId: string): ToolResultNode => ({
|
||||
kind: 'tool-result', seq: 3, callId,
|
||||
kind: 'tool-result', seq: 3, time: 3_000, callId,
|
||||
call: { name: 'bash', argsRaw: '{"command":"make build","description":"Build"}' },
|
||||
callTime: 2_000,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
})
|
||||
|
||||
|
||||
@@ -12,12 +12,13 @@ import type { ToolRowOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/
|
||||
|
||||
const running = (over?: Partial<RunningToolCall>): RunningToolCall => ({
|
||||
callId: 'c1', name: 'bash', argsRaw: '{"command":"ls -la","description":"List files"}',
|
||||
turn: 1, step: 1, callView: null, ...over,
|
||||
turn: 1, step: 1, time: 1_000, callView: null, ...over,
|
||||
})
|
||||
|
||||
const result = (over?: Partial<ToolResultNode>): ToolResultNode => ({
|
||||
kind: 'tool-result', seq: 10, callId: 'c1',
|
||||
kind: 'tool-result', seq: 10, time: 2_000, callId: 'c1',
|
||||
call: { name: 'bash', argsRaw: '{"command":"ls -la","description":"List files"}' },
|
||||
callTime: 1_000,
|
||||
content: [], isError: false, callView: null, resultView: null, ...over,
|
||||
})
|
||||
|
||||
|
||||
@@ -54,18 +54,19 @@ function makeSource(init?: Partial<ConversationSnapshot>) {
|
||||
}
|
||||
|
||||
const user = (seq: number, text: string): UserMessageNode => ({
|
||||
kind: 'user', seq, content: [{ type: 'text', text }] as never, source: null,
|
||||
kind: 'user', seq, time: seq * 1_000, content: [{ type: 'text', text }] as never, source: null,
|
||||
})
|
||||
const assistant = (seq: number, text: string): AssistantMessageNode => ({
|
||||
kind: 'assistant', seq, turn: 1, step: 1, blocks: [{ kind: 'text', text }],
|
||||
kind: 'assistant', seq, time: seq * 1_000, turn: 1, step: 1, blocks: [{ kind: 'text', text }],
|
||||
})
|
||||
const toolResult = (seq: number, callId: string, name = 'bash'): ToolResultNode => ({
|
||||
kind: 'tool-result', seq, callId,
|
||||
kind: 'tool-result', seq, time: seq * 1_000, callId,
|
||||
call: { name, argsRaw: `{"command":"cmd-${callId}","description":"run ${callId}"}` },
|
||||
callTime: seq * 1_000 - 500,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
})
|
||||
const runningCall = (callId: string, name = 'bash'): RunningToolCall => ({
|
||||
callId, name, argsRaw: `{"command":"cmd-${callId}"}`, turn: 2, step: 1, callView: null,
|
||||
callId, name, argsRaw: `{"command":"cmd-${callId}"}`, turn: 2, step: 1, time: 1_000, callView: null,
|
||||
})
|
||||
|
||||
/** Empty sessions-list hook stub (the global standard-kit seat; engines carry no hook since the store migration — bind here). */
|
||||
|
||||
@@ -62,8 +62,9 @@ describe('tails', () => {
|
||||
|
||||
it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
|
||||
const settled: ToolResultNode = {
|
||||
kind: 'tool-result', seq: 2, callId: 'c5',
|
||||
kind: 'tool-result', seq: 2, time: 2_000, callId: 'c5',
|
||||
call: { name: 'todo_write', argsRaw: '{"note":"x"}' },
|
||||
callTime: 1_000,
|
||||
content: [], isError: false, callView: null, resultView: null,
|
||||
}
|
||||
const props: ToolRowOwnerProps = {
|
||||
@@ -77,8 +78,9 @@ describe('tails', () => {
|
||||
|
||||
it('BashRow shows the failed pill on error results (root session arm)', () => {
|
||||
const errorResult: ToolResultNode = {
|
||||
kind: 'tool-result', seq: 1, callId: 'c1',
|
||||
kind: 'tool-result', seq: 1, time: 1_000, callId: 'c1',
|
||||
call: { name: 'bash', argsRaw: '{"command":"boom"}' },
|
||||
callTime: 500,
|
||||
content: [], isError: true, callView: null, resultView: null,
|
||||
}
|
||||
// Root session (no parentId): the global arm renders, error pill visible.
|
||||
|
||||
@@ -165,7 +165,7 @@ describe('DetailsPanel branches', () => {
|
||||
|
||||
it('shows non-JSON args verbatim (streaming fragment path)', () => {
|
||||
const view = panel({ turnSeq: 1, callId: 'c1', toolName: 'bash' }, {
|
||||
runningCalls: [{ callId: 'c1', name: 'bash', argsRaw: '{"cmd": tru', turn: 1, step: 1, callView: null }],
|
||||
runningCalls: [{ callId: 'c1', name: 'bash', argsRaw: '{"cmd": tru', turn: 1, step: 1, time: 1_000, callView: null }],
|
||||
})
|
||||
expect(view.getByText('{"cmd": tru')).toBeTruthy()
|
||||
})
|
||||
@@ -176,7 +176,7 @@ describe('DetailsPanel branches', () => {
|
||||
})
|
||||
|
||||
it('snapshot updates re-run the material selector through the shallow equality arm', () => {
|
||||
let snap = { ...snapshotBase(), runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{"a":1}', turn: 1, step: 1, callView: null }] } as ConversationSnapshot
|
||||
let snap = { ...snapshotBase(), runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{"a":1}', turn: 1, step: 1, time: 1_000, callView: null }] } as ConversationSnapshot
|
||||
const subs = new Set<() => void>()
|
||||
const source = {
|
||||
getSnapshot: () => snap,
|
||||
|
||||
@@ -234,10 +234,11 @@ describe('DetailsPanel', () => {
|
||||
it('renders the selected call args and result off the shared store; close fires the injected callback', () => {
|
||||
const { closeDetails } = benchDetails({
|
||||
nodes: [{
|
||||
kind: 'tool-result', callId: 'c1',
|
||||
kind: 'tool-result', seq: 1, time: 1_000, callId: 'c1',
|
||||
call: { name: 'bash', argsRaw: '{"cmd":"ls"}' },
|
||||
callTime: 500,
|
||||
content: [{ type: 'text', text: 'file-a\nfile-b' }],
|
||||
isError: false,
|
||||
isError: false, callView: null, resultView: null,
|
||||
}],
|
||||
}, { turnSeq: 1, callId: 'c1' })
|
||||
expect(screen.getByText('bash')).toBeTruthy()
|
||||
@@ -248,10 +249,10 @@ describe('DetailsPanel', () => {
|
||||
})
|
||||
|
||||
it('shows the empty hint without a selection and the running state for open calls', () => {
|
||||
benchDetails({ runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{}' }] }, null)
|
||||
benchDetails({ runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{}', turn: 1, step: 1, time: 1_000, callView: null }] }, null)
|
||||
expect(screen.getByText(/点击消息流中的工具行/)).toBeTruthy()
|
||||
cleanup()
|
||||
benchDetails({ runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{}' }] }, { turnSeq: 1, callId: 'c9' })
|
||||
benchDetails({ runningCalls: [{ callId: 'c9', name: 'bash', argsRaw: '{}', turn: 1, step: 1, time: 1_000, callView: null }] }, { turnSeq: 1, callId: 'c9' })
|
||||
expect(screen.getByText('运行中…')).toBeTruthy()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user