Merge latest master into PR 555

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/runtime/README.i18n.yaml
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/MessageItem.tsx
#	packages/compact/compact-basic/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
creatixchu
2026-07-31 19:39:22 +08:00
187 changed files with 5806 additions and 657 deletions

View File

@@ -38,17 +38,22 @@ function request(
resultSeq?: number,
replacementSeq?: number,
): RequestView {
return {
purpose,
const base = {
startSeq,
turn: 1,
step: purpose === 'assistant' ? 1 : 0,
startedAt: startSeq,
completedAt: startSeq + 1,
status: 'complete',
status: 'complete' as const,
...(resultSeq === undefined ? {} : { resultSeq }),
...(replacementSeq === undefined ? {} : { replacementSeq }),
}
return purpose === 'assistant'
? { ...base, purpose, turn: 1, step: 1 }
: {
...base,
purpose,
turn: 1,
step: 0,
...(replacementSeq === undefined ? {} : { replacementSeq }),
}
}
describe('trajectory context branches', () => {

View File

@@ -5,7 +5,9 @@
*/
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
import type {
ConversationSnapshot, RequestView,
} from '@deepseek-ai/dsh-client-runtime/client'
import { TrajectoryGroupHeader } from '../src/client/TrajectoryGroupHeader.tsx'
import { TrajectoryTurn } from '../src/client/TrajectoryTurn.tsx'
import { TrajectoryTurnHeader } from '../src/client/TrajectoryTurnHeader.tsx'
@@ -161,6 +163,49 @@ describe('deriveTrajectoryLayout', () => {
expect(turns[1]?.groups.flatMap(g => g.cells.map(c => c.text))).toEqual(['second', 'ok2'])
})
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 },
{
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1,
blocks: [{ kind: 'text', text: 'before compaction' }],
},
{ kind: 'user', seq: 5, time: 5_000, content: [{ type: 'text', text: 'second' }], source: null },
{
kind: 'assistant', seq: 6, time: 6_000, turn: 2, step: 1,
blocks: [{ kind: 'text', text: 'after compaction' }],
},
] as unknown as ConversationSnapshot['nodes']
const compaction: RequestView = {
purpose: 'compaction',
startSeq: 3,
turn: null,
step: 0,
startedAt: 3_000,
completedAt: 4_000,
status: 'complete',
summary: [{ type: 'text', text: 'standalone summary' }],
}
const turns = deriveTrajectoryLayout({
codeDispatches: new Map(),
nodes,
partial: null,
runningCalls: [],
requests: [compaction],
})
expect(turns.map(turn => turn.turn)).toEqual([1, null, 2])
expect(turns[1]?.groups).toMatchObject([{
title: 'Compaction 3',
cells: [{
kind: 'compacted',
sourceSeq: 3,
text: 'standalone summary',
}],
}])
})
it('keeps usage and a meaningful summary when assistant has no text block', () => {
const nodes = [
{

View File

@@ -290,6 +290,105 @@ describe('tab switching in ConversationRoot', () => {
expect(screen.queryByRole('complementary', { name: 'Event details' })).toBeNull()
})
it('labels a standalone compaction as between-turn work in the ledger and inspector', async () => {
const nodes = [
{ kind: 'user', seq: 1, time: 1_000, content: [], source: null },
{
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1,
blocks: [{ kind: 'text', text: 'before' }],
},
{ kind: 'user', seq: 5, time: 5_000, content: [], source: null },
{
kind: 'assistant', seq: 6, time: 6_000, turn: 2, step: 1,
blocks: [{ kind: 'text', text: 'after' }],
},
] as unknown as ConversationSnapshot['nodes']
const compaction: RequestView = {
purpose: 'compaction',
startSeq: 3,
turn: null,
step: 0,
startedAt: 3_000,
completedAt: 4_000,
status: 'complete',
summary: [{ type: 'text', text: 'standalone summary' }],
}
const b = await bench(historySnapshot(nodes, { requests: [compaction] }))
const view = mount(b.slots, nodes)
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
expect(screen.getByText('Between turns')).toBeTruthy()
expect(view.container.textContent).not.toContain('Turn null')
fireEvent.click(screen.getByRole('button', { name: 'Request #2 · Compaction' }))
expect(screen.getByText('Compaction · Between turns')).toBeTruthy()
expect(view.container.textContent).not.toContain('Turn null')
})
it('activates only the selected standalone compaction section', async () => {
const nodes = [
{ kind: 'user', seq: 1, time: 1_000, content: [], source: null },
{
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1,
blocks: [{ kind: 'text', text: 'before first compaction' }],
},
{ kind: 'user', seq: 5, time: 5_000, content: [], source: null },
{
kind: 'assistant', seq: 6, time: 6_000, turn: 2, step: 1,
blocks: [{ kind: 'text', text: 'between compactions' }],
},
{ kind: 'user', seq: 9, time: 9_000, content: [], source: null },
{
kind: 'assistant', seq: 10, time: 10_000, turn: 3, step: 1,
blocks: [{ kind: 'text', text: 'after second compaction' }],
},
] as unknown as ConversationSnapshot['nodes']
const compactions: RequestView[] = [
{
purpose: 'compaction',
startSeq: 3,
turn: null,
step: 0,
startedAt: 3_000,
completedAt: 4_000,
status: 'complete',
summary: [{ type: 'text', text: 'first standalone summary' }],
},
{
purpose: 'compaction',
startSeq: 7,
turn: null,
step: 0,
startedAt: 7_000,
completedAt: 8_000,
status: 'complete',
summary: [{ type: 'text', text: 'second standalone summary' }],
},
]
const b = await bench(historySnapshot(nodes, { requests: compactions }))
mount(b.slots, nodes)
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
const firstRequest = screen.getByRole('button', { name: 'Request #2 · Compaction' })
const secondRequest = screen.getByRole('button', { name: 'Request #4 · Compaction' })
const firstSection = firstRequest.closest('tr')?.querySelector('span')
const secondSection = secondRequest.closest('tr')?.querySelector('span')
expect(firstSection?.textContent).toBe('Between turns')
expect(secondSection?.textContent).toBe('Between turns')
fireEvent.click(firstRequest)
expect(firstSection?.className).toMatch(/turnLabelActive/)
expect(secondSection?.className).not.toMatch(/turnLabelActive/)
expect(screen.getByText('Request #2')).toBeTruthy()
expect(screen.getByText('Compaction · Between turns')).toBeTruthy()
fireEvent.click(secondRequest)
expect(firstSection?.className).not.toMatch(/turnLabelActive/)
expect(secondSection?.className).toMatch(/turnLabelActive/)
expect(screen.getByText('Request #4')).toBeTruthy()
expect(screen.getByText('Compaction · Between turns')).toBeTruthy()
})
it('dragging the overview focuses overlapping records without filtering the ledger', async () => {
const b = await bench()
mount(b.slots)
@@ -563,6 +662,44 @@ describe('timeline projection', () => {
})
})
it('projects between-turn compaction without inventing a turn boundary', () => {
const withStandaloneCompaction = [
{
turn: 1,
groups: [{
title: 'Step 1',
cells: [{ index: 1, kind: 'message', text: 'before', timeSeconds: 0 }],
}],
},
{
turn: null,
groups: [{
title: 'Compaction 3',
cells: [{ index: 2, kind: 'compacted', text: 'summary', timeSeconds: 0 }],
}],
},
{
turn: 2,
groups: [{
title: 'Step 1',
cells: [{ index: 3, kind: 'message', text: 'after', timeSeconds: 0 }],
}],
},
] satisfies readonly TrajectoryTurnModel[]
expect(deriveTrajectoryTimeline(withStandaloneCompaction)).toMatchObject({
spans: [
{ index: 1, start: 0, end: 1 },
{ index: 2, start: 1, end: 2 },
{ index: 3, start: 2, end: 3 },
],
turnBoundaries: [
{ turn: 1, time: 0 },
{ turn: 2, time: 2 },
],
})
})
it('empty inputs produce no model and the standalone view reports its empty form', () => {
expect(deriveTrajectoryTimeline([])).toBeNull()
render(createElement(