feat(ui): render todo plans and streamed tool kinds in both views

todo/write events fold into plan targets: the chat transcript and
trajectory show a checklist card (done counts, active item), and live
ACP plan updates render the same card while the turn streams — the
task list was previously invisible on both paths. Tool rows label
themselves with the streamed ACP kind verb (read/edit/search/run…)
instead of a generic noun, and touched-file locations open in the OS
editor from the expanded row.
This commit is contained in:
NI0317
2026-07-20 10:25:11 +08:00
parent 7a5f0b0827
commit cdb5524632
6 changed files with 257 additions and 9 deletions

View File

@@ -256,6 +256,10 @@ describe('desktop renderer chat lifecycle', () => {
...turnEvents(3, 'third', 'third answer', 20),
{ type: 'tool/call', seq: 30, time: 31, data: { turn: 3, step: 1, callId: 'wf-1', name: 'workflow', arguments: '{"name":"audit"}' } },
{ type: 'tool/result', seq: 31, time: 32, data: { turn: 3, step: 1, callId: 'wf-1', content: [{ type: 'text', text: 'done' }] } },
{ type: 'todo/write', seq: 32, time: 33, data: { turn: 3, step: 1, todos: [
{ content: 'collect findings', status: 'completed' },
{ content: 'write the report', status: 'in_progress' },
] } },
])
traceRead = turn1Trace
@@ -318,8 +322,17 @@ describe('desktop renderer chat lifecycle', () => {
await vi.waitFor(() => {
expect(document.querySelector('#liveTurn .user-bubble')?.textContent).toBe('third')
})
// The ACP stream carries a richer tool title than the persisted name.
update?.({ sessionId: 's-lag', update: { sessionUpdate: 'tool_call', toolCallId: 'wf-1', title: 'workflow: run audit agents', status: 'in_progress' } })
// The ACP stream carries a richer tool title, a kind, and a plan snapshot.
update?.({ sessionId: 's-lag', update: { sessionUpdate: 'tool_call', toolCallId: 'wf-1', title: 'workflow: run audit agents', kind: 'execute', status: 'in_progress' } })
update?.({ sessionId: 's-lag', update: { sessionUpdate: 'plan', entries: [
{ content: 'collect findings', priority: 'medium', status: 'in_progress' },
{ content: 'write the report', priority: 'medium', status: 'pending' },
] } })
await vi.waitFor(() => {
const livePlan = document.querySelector('[data-live="plan"] .plan-card')
expect(livePlan?.textContent).toContain('collect findings')
expect(livePlan?.textContent).toContain('0/2')
})
// Once the persisted log catches up, the view converges with no user action.
prompts[2]!.resolve({ response: {}, trace: turn1Trace })
@@ -330,5 +343,10 @@ describe('desktop renderer chat lifecycle', () => {
}, { timeout: 4000 })
// The live workflow presentation survives the switch to the persisted view.
expect(document.querySelector('#conversation')?.textContent).toContain('workflow: run audit agents')
// The persisted todo/write renders as a checklist card with the streamed kind verb.
const planCard = document.querySelector('#conversation .plan-activity .plan-card')
expect(planCard?.textContent).toContain('write the report')
expect(planCard?.textContent).toContain('1/2')
expect(document.querySelector('#conversation .chat-activity.tool-use .activity-select span')?.textContent).toBe('执行')
})
})

View File

@@ -32,6 +32,30 @@ describe('desktop trace graph', () => {
])
})
it('folds todo/write events into plan targets and chat activities', () => {
const graph = buildTraceGraph('s-plan', [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message' } } },
{ type: 'user/message', seq: 1, time: 2, data: { content: [{ type: 'text', text: 'go' }] } },
{ type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } },
{ type: 'todo/write', seq: 3, time: 4, data: { turn: 1, step: 1, todos: [
{ content: 'read the code', status: 'completed' },
{ content: 'fix the bug', status: 'in_progress' },
] } },
{ type: 'todo/write', seq: 4, time: 5, data: { turn: 1, step: 1 } },
{ type: 'step/end', seq: 5, time: 6, data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 6, time: 7, data: { turn: 1, reason: { kind: 'completed' } } },
])
const plan = graph.targets.get('plan:3')!
expect(plan.kind).toBe('plan')
expect(plan.output).toEqual([
{ content: 'read the code', status: 'completed' },
{ content: 'fix the bug', status: 'in_progress' },
])
expect(graph.targets.get('plan:4')?.output).toEqual([])
expect(graph.chatTurns[0]?.activities).toContainEqual({ kind: 'plan', targetId: 'plan:3' })
expect(graph.trajectoryRows.filter(row => row.targetId === 'plan:3')).toHaveLength(1)
})
it('normalizes incomplete and malformed event tails without inventing duplicate rows', () => {
expect(buildTraceGraph('empty', []).startTime).toBe(0)
const graph = buildTraceGraph('edge', [