Why: Process output folded on every text delta, successful tool results disappeared after refresh, and generated files stayed plain text instead of a card that opens a closable preview. Evidence: npx tsc -b --noEmit (clean); npx vitest run (51 passed). Impact: Live work stays expanded until the run ends, then folds to one worked-time line. File cards open a per-conversation preview pane. docx renders page by page, with mammoth if that fails. Refs: feature/chat-timeline-preview Co-authored-by: Cursor <cursoragent@cursor.com>
63 lines
3.2 KiB
TypeScript
63 lines
3.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { toolKind, toolHeadline, toolGroupLabel, timelineEntries } from '../src/toolkind'
|
|
import type { Part } from '../src/contracts'
|
|
|
|
function p(partial: Partial<Part> & { type: Part['type'] }): Part {
|
|
return partial as Part
|
|
}
|
|
|
|
describe('toolKind', () => {
|
|
it('classifies known tools and falls back to other', () => {
|
|
expect(toolKind('fs_read_file')).toBe('read')
|
|
expect(toolKind('web_search')).toBe('search')
|
|
expect(toolKind('run_python')).toBe('exec')
|
|
expect(toolKind('register_artifact')).toBe('write')
|
|
expect(toolKind('submit_card')).toBe('card')
|
|
expect(toolKind('delegate_task')).toBe('delegate')
|
|
expect(toolKind('update_todos')).toBe('plan')
|
|
expect(toolKind('time_now')).toBe('other')
|
|
})
|
|
|
|
it('pulls a path, a query, or the first script line out of arguments', () => {
|
|
expect(toolHeadline('fs_read_file', '{"path":"notes/a.md"}')).toBe('notes/a.md')
|
|
expect(toolHeadline('web_search', '{"query":"会议冲突"}')).toBe('会议冲突')
|
|
expect(toolHeadline('run_python', '{"script":"print(1)\\nprint(2)"}')).toBe('print(1)')
|
|
expect(toolGroupLabel('read', 2, false)).toBe('查阅 · 2 个文件')
|
|
expect(toolGroupLabel('exec', 1, true)).toBe('运行命令 · 1 条')
|
|
})
|
|
})
|
|
|
|
describe('timelineEntries', () => {
|
|
it('merges consecutive tools of the same kind and leaves narration in place', () => {
|
|
const parts: Part[] = [
|
|
p({ type: 'thinking', process: true, text: '先看文件', durationMs: 1500 }),
|
|
p({ type: 'tool_call', process: true, toolCall: { id: 'a', name: 'fs_read_file', arguments: '{"path":"a.md"}' } }),
|
|
p({ type: 'tool_result', process: true, toolResult: { callId: 'a', status: 'ok', summary: '12 行' } }),
|
|
p({ type: 'tool_call', process: true, toolCall: { id: 'b', name: 'fs_read_file', arguments: '{"path":"b.md"}' } }),
|
|
p({ type: 'tool_result', process: true, toolResult: { callId: 'b', status: 'ok' } }),
|
|
p({ type: 'text', process: true, text: '两份都读完了' }),
|
|
p({ type: 'tool_call', process: true, toolCall: { id: 'c', name: 'run_python', arguments: '{"script":"print(1)"}' } }),
|
|
]
|
|
const rows = timelineEntries(parts, true)
|
|
expect(rows.map(r => r.kind)).toEqual(['thinking', 'tools', 'text', 'tools'])
|
|
const reads = rows[1]
|
|
if (reads.kind !== 'tools') throw new Error('expected tool group')
|
|
expect(reads.toolKind).toBe('read')
|
|
expect(reads.items.map(it => it.headline)).toEqual(['a.md', 'b.md'])
|
|
expect(reads.items[0].summary).toBe('12 行')
|
|
const thinking = rows[0]
|
|
if (thinking.kind !== 'thinking') throw new Error('expected thinking')
|
|
expect(thinking.active).toBe(false)
|
|
const exec = rows[3]
|
|
if (exec.kind !== 'tools') throw new Error('expected exec group')
|
|
expect(exec.items[0].running).toBe(true)
|
|
})
|
|
|
|
it('marks the trailing thinking row active while the run is live', () => {
|
|
const rows = timelineEntries([p({ type: 'thinking', process: true, text: '还在想' })], true)
|
|
expect(rows[0]).toMatchObject({ kind: 'thinking', active: true })
|
|
expect(timelineEntries([p({ type: 'thinking', process: true, text: '想完了' })], false)[0])
|
|
.toMatchObject({ kind: 'thinking', active: false })
|
|
})
|
|
})
|