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:
@@ -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('执行')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', [
|
||||
|
||||
Reference in New Issue
Block a user