mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/config-catalog.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/llm-streaming.i18n.yaml # docs/module-graph.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/index.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-trajectory/tests/views.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/fetch-carrier.spec.ts # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-deepseek/tests/adapter.spec.ts # packages/llm/llm-deepseek/tests/serialize.spec.ts # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-pi-ai/src/index.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/src/types.ts # packages/ui/tui/README.i18n.yaml # packages/ui/tui/src/index.ts # packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
@@ -16,11 +16,11 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
|
||||
subtool: 'Sub',
|
||||
}
|
||||
|
||||
const TAG_CLASS: Record<TrajectoryCellKind, string> = {
|
||||
user: css.tagUser!,
|
||||
message: css.tagMessage!,
|
||||
tool: css.tagTool!,
|
||||
subtool: css.tagSubtool!,
|
||||
const TAG_CLASS: Record<TrajectoryCellKind, string | undefined> = {
|
||||
user: css.tagUser,
|
||||
message: css.tagMessage,
|
||||
tool: css.tagTool,
|
||||
subtool: css.tagSubtool,
|
||||
}
|
||||
|
||||
export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
|
||||
@@ -84,7 +84,7 @@ export function TrajectoryCell({
|
||||
<div className={rootClass} data-kind={kind} data-selected={selected || undefined} {...rest}>
|
||||
<span className={css.index}>#{index}</span>
|
||||
<span className={css.tagSlot}>
|
||||
<span className={`${css.tag} ${TAG_CLASS[kind]}`}>{KIND_LABEL[kind]}</span>
|
||||
<span className={[css.tag, TAG_CLASS[kind]].filter((c): c is string => c !== undefined).join(' ')}>{KIND_LABEL[kind]}</span>
|
||||
</span>
|
||||
<span className={css.text}>{text}</span>
|
||||
<span className={css.trailing}>
|
||||
|
||||
@@ -14,7 +14,7 @@ import css from './TrajectoryStatsHeader.module.css'
|
||||
export interface TrajectoryStatsHeaderProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
|
||||
|
||||
export const TrajectoryStatsHeader = memo(function TrajectoryStatsHeader({ useSession }: TrajectoryStatsHeaderProps) {
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const stats = useMemo(() => deriveSpanStats(deriveSpans(nodes)), [nodes])
|
||||
if (stats.turns === 0) return null
|
||||
return <div className={css.root}>{`${stats.turns} turns · ${stats.steps} steps · ${stats.calls} tool calls`}</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export function TrajectoryTurnHeader({ turn }: TrajectoryTurnHeaderProps) {
|
||||
<div className={css.inner}>
|
||||
<span className={css.title}>Turn {turn}</span>
|
||||
<div className={css.columns} aria-hidden="true">
|
||||
{COLUMN_LABELS.map((label) => (
|
||||
{COLUMN_LABELS.map(label => (
|
||||
<span key={label} className={css.column}>{label}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,10 @@ import { deriveTrajectoryLayout } from './layout.ts'
|
||||
import css from './views.module.css'
|
||||
|
||||
export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const partial = useSession((s) => s.partial)
|
||||
const runningCalls = useSession((s) => s.runningCalls)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const partial = useSession(s => s.partial)
|
||||
const runningCalls = useSession(s => s.runningCalls)
|
||||
const codeDispatches = useSession(s => s.codeDispatches)
|
||||
const turns = useMemo(
|
||||
() => deriveTrajectoryLayout({ nodes, partial, runningCalls, codeDispatches }),
|
||||
[nodes, partial, runningCalls, codeDispatches],
|
||||
@@ -22,15 +22,15 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
|
||||
}
|
||||
return (
|
||||
<div className={css.root}>
|
||||
{turns.map((turn) => (
|
||||
{turns.map(turn => (
|
||||
<TrajectoryTurn key={turn.turn} turn={turn.turn}>
|
||||
{turn.groups.flatMap((group) => [
|
||||
{turn.groups.flatMap(group => [
|
||||
<TrajectoryGroupHeader
|
||||
key={`${group.title}-h`}
|
||||
title={group.title}
|
||||
{...(group.description !== undefined ? { description: group.description } : {})}
|
||||
/>,
|
||||
...group.cells.map((cell) => (
|
||||
...group.cells.map(cell => (
|
||||
<TrajectoryCell key={cell.index} {...cell} />
|
||||
)),
|
||||
])}
|
||||
|
||||
@@ -24,8 +24,8 @@ export interface WaterfallExtraProps {
|
||||
|
||||
export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & WaterfallExtraProps) {
|
||||
const scale = pxPerNode ?? PX_PER_NODE
|
||||
const nodes = useSession((s) => s.nodes)
|
||||
const codeDispatches = useSession((s) => s.codeDispatches)
|
||||
const nodes = useSession(s => s.nodes)
|
||||
const codeDispatches = useSession(s => s.codeDispatches)
|
||||
const spans = useMemo(() => deriveSpans(nodes), [nodes])
|
||||
const subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
|
||||
if (spans.length === 0) return <div className={css.root}><p className={css.empty}>暂无瀑布数据</p></div>
|
||||
@@ -50,7 +50,7 @@ export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & Waterfa
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(subSpans.get(span.turn) ?? []).map((lane) => (
|
||||
{(subSpans.get(span.turn) ?? []).map(lane => (
|
||||
<div key={lane.callId} className={css.subRow} data-subspan style={{ paddingLeft: i * 12 + 24 }}>
|
||||
<span className={css.subTag}>{lane.name}</span>
|
||||
<span
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('TrajectoryCell', () => {
|
||||
expect(screen.getByText('381')).toBeTruthy()
|
||||
expect(screen.getByText('155')).toBeTruthy()
|
||||
expect(screen.getByText('+235.2s')).toBeTruthy()
|
||||
const texts = [...container.querySelectorAll('span')].map((el) => el.textContent)
|
||||
const texts = [...container.querySelectorAll('span')].map(el => el.textContent)
|
||||
expect(texts.indexOf('136')).toBeLessThan(texts.indexOf('381'))
|
||||
expect(texts.indexOf('381')).toBeLessThan(texts.indexOf('155'))
|
||||
expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('+235.2s'))
|
||||
|
||||
@@ -73,13 +73,13 @@ describe('deriveTrajectoryLayout', () => {
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
expect(turns).toHaveLength(1)
|
||||
expect(turns[0]?.turn).toBe(1)
|
||||
const kinds = turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.kind))
|
||||
const kinds = turns[0]?.groups.flatMap(g => g.cells.map(c => c.kind))
|
||||
expect(kinds).toEqual(['user', 'message', 'tool'])
|
||||
const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
|
||||
const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
|
||||
expect(message).toMatchObject({
|
||||
input: 10, output: 20, think: 5, timeSeconds: 5,
|
||||
})
|
||||
const tool = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'tool')
|
||||
const tool = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'tool')
|
||||
expect(tool?.text).toBe('bash · {"command":"ls"}')
|
||||
expect(tool?.timeSeconds).toBe(1.3)
|
||||
})
|
||||
@@ -87,14 +87,14 @@ describe('deriveTrajectoryLayout', () => {
|
||||
it('adds runningCalls not already present and leaves their time blank', () => {
|
||||
const turns = deriveTrajectoryLayout({
|
||||
codeDispatches: new Map(),
|
||||
nodes: [] as unknown as ConversationSnapshot['nodes'],
|
||||
nodes: [],
|
||||
partial: null,
|
||||
runningCalls: [{
|
||||
callId: 'r1', name: 'bash', argsRaw: '{"command":"pwd"}',
|
||||
turn: 1, step: 2, time: 9_000, callView: null,
|
||||
}],
|
||||
})
|
||||
expect(turns[0]?.groups.map((g) => g.title)).toEqual(['Step 2'])
|
||||
expect(turns[0]?.groups.map(g => g.title)).toEqual(['Step 2'])
|
||||
expect(turns[0]?.groups[0]?.cells[0]).toMatchObject({
|
||||
kind: 'tool', text: 'bash · {"command":"pwd"}', timeSeconds: null,
|
||||
})
|
||||
@@ -113,9 +113,9 @@ describe('deriveTrajectoryLayout', () => {
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const cells = turns[0]?.groups.flatMap((g) => g.cells) ?? []
|
||||
expect(cells.find((c) => c.kind === 'message')?.timeSeconds).toBeNull()
|
||||
expect(turns[0]?.groups.find((g) => g.title === 'Step 1')?.description).toBeUndefined()
|
||||
const cells = turns[0]?.groups.flatMap(g => g.cells) ?? []
|
||||
expect(cells.find(c => c.kind === 'message')?.timeSeconds).toBeNull()
|
||||
expect(turns[0]?.groups.find(g => g.title === 'Step 1')?.description).toBeUndefined()
|
||||
})
|
||||
|
||||
it('builds a wall-span step description with a tool histogram', () => {
|
||||
@@ -156,9 +156,9 @@ describe('deriveTrajectoryLayout', () => {
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
expect(turns.map((t) => t.turn)).toEqual([1, 2])
|
||||
expect(turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['first', 'ok1'])
|
||||
expect(turns[1]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['second', 'ok2'])
|
||||
expect(turns.map(t => t.turn)).toEqual([1, 2])
|
||||
expect(turns[0]?.groups.flatMap(g => g.cells.map(c => c.text))).toEqual(['first', 'ok1'])
|
||||
expect(turns[1]?.groups.flatMap(g => g.cells.map(c => c.text))).toEqual(['second', 'ok2'])
|
||||
})
|
||||
|
||||
it('keeps usage on the fallback Message row when assistant has no text block', () => {
|
||||
@@ -170,7 +170,7 @@ describe('deriveTrajectoryLayout', () => {
|
||||
},
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
|
||||
const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
|
||||
expect(message).toMatchObject({
|
||||
text: '', input: 11, output: 22, think: 3,
|
||||
})
|
||||
@@ -199,8 +199,8 @@ describe('deriveTrajectoryLayout', () => {
|
||||
] as unknown as ConversationSnapshot['nodes']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
|
||||
const message = turns[0]?.groups
|
||||
.flatMap((g) => g.cells)
|
||||
.find((c) => c.kind === 'message' && c.text === 'done')
|
||||
.flatMap(g => g.cells)
|
||||
.find(c => c.kind === 'message' && c.text === 'done')
|
||||
// From context at 9s, not from the earlier user/tool surfaces.
|
||||
expect(message?.timeSeconds).toBe(1)
|
||||
})
|
||||
@@ -234,10 +234,10 @@ describe('run_code sub-dispatch cells', () => {
|
||||
settledSub(2, 'read', 7_300, 7_800),
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
|
||||
const cells = turns[0]!.groups.flatMap((g) => g.cells)
|
||||
expect(cells.map((c) => c.kind)).toEqual(['tool', 'subtool', 'subtool'])
|
||||
const cells = turns[0]!.groups.flatMap(g => g.cells)
|
||||
expect(cells.map(c => c.kind)).toEqual(['tool', 'subtool', 'subtool'])
|
||||
// Sequential indexes across the interleave; durations from the pair times.
|
||||
expect(cells.map((c) => c.index)).toEqual([1, 2, 3])
|
||||
expect(cells.map(c => c.index)).toEqual([1, 2, 3])
|
||||
expect(cells[1]).toMatchObject({ text: 'bash · {"x":1}', timeSeconds: 1 })
|
||||
expect(cells[2]).toMatchObject({ timeSeconds: 0.5 })
|
||||
})
|
||||
@@ -249,7 +249,7 @@ describe('run_code sub-dispatch cells', () => {
|
||||
}
|
||||
const codeDispatches = new Map([['p1', [running]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
|
||||
const sub = turns[0]!.groups.flatMap((g) => g.cells).find((c) => c.kind === 'subtool')
|
||||
const sub = turns[0]!.groups.flatMap(g => g.cells).find(c => c.kind === 'subtool')
|
||||
expect(sub).toMatchObject({ text: 'grep · {"pattern":"x"}', timeSeconds: null })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -149,7 +149,7 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES
|
||||
})) as never}
|
||||
inputActions={{
|
||||
setDraft: vi.fn(), addImages: vi.fn(), removeImage: vi.fn(), pruneImages: vi.fn(), submit: vi.fn(),
|
||||
} as never}
|
||||
}}
|
||||
bindDraftMirror={() => () => {}}
|
||||
open={vi.fn()}
|
||||
/>,
|
||||
@@ -169,7 +169,7 @@ describe('plugin registration', () => {
|
||||
it('fiber disposal removes both tabs and leaves chat standing', async () => {
|
||||
const b = await bench()
|
||||
await b.fiber.dispose()
|
||||
expect(tabsOf(b.slots).map((v) => v.id)).toEqual(['chat'])
|
||||
expect(tabsOf(b.slots).map(v => v.id)).toEqual(['chat'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('tab switching in ConversationRoot', () => {
|
||||
const b = await bench()
|
||||
mount(b.slots)
|
||||
expect(screen.getByTestId('chat-body')).toBeTruthy()
|
||||
expect(screen.getAllByRole('tab').map((t) => t.textContent)).toEqual(['Chat', 'Trajectory', 'Waterfall'])
|
||||
expect(screen.getAllByRole('tab').map(t => t.textContent)).toEqual(['Chat', 'Trajectory', 'Waterfall'])
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
|
||||
expect(screen.queryByText(/turns ·/)).toBeNull()
|
||||
@@ -203,7 +203,7 @@ describe('tab switching in ConversationRoot', () => {
|
||||
|
||||
it('empty window: placeholder copy in the body, the stats header renders nothing', async () => {
|
||||
const b = await bench()
|
||||
mount(b.slots, [] as unknown as ConversationSnapshot['nodes'])
|
||||
mount(b.slots, [])
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
|
||||
expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
|
||||
expect(screen.queryByText(/turns ·/)).toBeNull()
|
||||
@@ -226,12 +226,12 @@ describe('span derivation', () => {
|
||||
})
|
||||
|
||||
it('empty inputs produce zero stats and standalone components render their empty forms', () => {
|
||||
expect(deriveSpanStats(deriveSpans([] as unknown as ConversationSnapshot['nodes']))).toEqual({ turns: 0, steps: 0, calls: 0 })
|
||||
const { useSession } = fakeSession([] as unknown as ConversationSnapshot['nodes'])
|
||||
const { container } = render(createElement(TrajectoryStatsHeader, { useSession: useSession as never }))
|
||||
expect(deriveSpanStats(deriveSpans([]))).toEqual({ turns: 0, steps: 0, calls: 0 })
|
||||
const { useSession } = fakeSession([])
|
||||
const { container } = render(createElement(TrajectoryStatsHeader, { useSession: useSession }))
|
||||
expect(container.firstChild).toBeNull()
|
||||
render(createElement(TrajectoryView as FC<ConvViewProps>,
|
||||
standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
|
||||
standaloneProps([])))
|
||||
expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -239,7 +239,7 @@ describe('span derivation', () => {
|
||||
describe('WaterfallView standalone branches', () => {
|
||||
it('empty window renders the placeholder copy', () => {
|
||||
render(createElement(WaterfallView as FC<ConvViewProps>,
|
||||
standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
|
||||
standaloneProps([])))
|
||||
expect(screen.getByText('暂无瀑布数据')).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -253,7 +253,7 @@ describe('WaterfallView standalone branches', () => {
|
||||
|
||||
describe('node half', () => {
|
||||
it('node apply is an intentional no-op (loader-managed lifecycle only)', () => {
|
||||
expect(nodeApply()).toBeUndefined()
|
||||
expect(() => { nodeApply() }).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('deriveSubSpans (waterfall lanes)', () => {
|
||||
{ callId: 'p1:code:2', name: 'grep', argsRaw: '{}', turn: 0, step: 0, time: 7_000, callView: null },
|
||||
]]]) as unknown as ConversationSnapshot['codeDispatches']
|
||||
const lanes = deriveSubSpans(dispatchNodes, codeDispatches)
|
||||
const running = lanes.get(3)?.find((lane) => lane.name === 'grep')
|
||||
const running = lanes.get(3)?.find(lane => lane.name === 'grep')
|
||||
expect(running).toMatchObject({ durationMs: null, timing: 'running' })
|
||||
// Extends from its start to the window end.
|
||||
expect(running!.offsetFraction + running!.widthFraction).toBeCloseTo(1)
|
||||
|
||||
Reference in New Issue
Block a user