import type { TimelineItem } from './types.js'; import { lastLines } from './format.js'; type ToolState = { timelineId: string; name: string; args: any; title: string; summary: string; }; export type EventRenderState = { thinkingId?: string; thinkingStartedAt?: number; thinkingText: string; thinkingLines: string[]; assistantId?: string; assistantText: string; tools: Map; }; export function createEventRenderState(): EventRenderState { return { thinkingText: '', thinkingLines: [], assistantText: '', tools: new Map() }; } export function reduceTaskEvent(items: TimelineItem[], state: EventRenderState, event: { type: string; data: any }): TimelineItem[] { const data = event.data || {}; switch (event.type) { case 'thinking_start': { const timelineId = newId(); state.thinkingId = timelineId; state.thinkingStartedAt = Date.now(); state.thinkingText = ''; state.thinkingLines = []; return [...items, { id: timelineId, kind: 'thinking', title: '思考中', status: 'running', lines: [] }]; } case 'thinking_chunk': { const content = String(data.content || ''); state.thinkingText += content; const lines = state.thinkingText.split(/\r?\n/).filter((line) => line.length > 0); state.thinkingLines = lines; if (!state.thinkingId) { state.thinkingId = newId(); state.thinkingStartedAt = Date.now(); state.thinkingText = content; items = [...items, { id: state.thinkingId, kind: 'thinking', title: '思考中', status: 'running', lines: [] }]; } return patchItem(items, state.thinkingId, { lines: lastLines(state.thinkingLines, 5) }); } case 'thinking_end': { if (!state.thinkingId) return items; const elapsed = state.thinkingStartedAt ? `${((Date.now() - state.thinkingStartedAt) / 1000).toFixed(1)}s` : ''; const id = state.thinkingId; state.thinkingId = undefined; state.thinkingStartedAt = undefined; state.thinkingText = ''; state.thinkingLines = []; return patchItem(items, id, { title: `思考完成 ${elapsed}`.trim(), status: 'success', body: undefined, summary: undefined, lines: [], }); } case 'text_start': { const timelineId = newId(); state.assistantId = timelineId; state.assistantText = ''; return [...items, { id: timelineId, kind: 'assistant', status: 'running', body: '' }]; } case 'text_chunk': { const content = String(data.content || ''); if (!state.assistantId) { state.assistantId = newId(); state.assistantText = ''; items = [...items, { id: state.assistantId, kind: 'assistant', status: 'running', body: '' }]; } state.assistantText += content; return patchItem(items, state.assistantId, { body: state.assistantText }); } case 'text_end': { const full = String(data.full_content || state.assistantText || ''); if (!state.assistantId) return full ? [...items, { id: newId(), kind: 'assistant', title: '完成', status: 'success', body: full }] : items; const id = state.assistantId; state.assistantId = undefined; state.assistantText = ''; return patchItem(items, id, { status: 'success', body: full }); } case 'tool_preparing': case 'tool_start': { const toolId = String(data.id || data.execution_id || data.preparing_id || newId()); const name = String(data.name || 'tool'); const args = data.arguments || {}; const title = toolTitle(name, args, 'running'); const summary = toolSummaryPreview(data, name, args, 'running'); const existingKey = data.preparing_id ? String(data.preparing_id) : toolId; const existing = state.tools.get(existingKey); if (existing) { state.tools.delete(existingKey); state.tools.set(toolId, { ...existing, name, args, title, summary }); return patchItem(items, existing.timelineId, { title, summary, status: 'running', body: toolInitialBody(name, args), lines: undefined, }); } const timelineId = newId(); state.tools.set(toolId, { timelineId, name, args, title, summary }); return [...items, { id: timelineId, kind: 'tool', title, summary, status: 'running', body: toolInitialBody(name, args) }]; } case 'tool_update_action': case 'update_action': { const toolId = String(data.id || data.execution_id || data.preparing_id || ''); const tool = state.tools.get(toolId); if (!tool) return items; if (data.result !== undefined || data.status) { const status = String(data.status || '').toLowerCase() === 'completed' ? 'success' : String(data.status || '').toLowerCase() === 'failed' ? 'failed' : 'running'; return patchItem(items, tool.timelineId, { title: toolTitle(tool.name, tool.args, status), summary: toolSummaryPreview(data, tool.name, tool.args, status), status, body: toolResultBody(tool.name, tool.args, data.result), lines: toolResultLines(tool.name, tool.args, data.result), }); } return items; } case 'append_payload': case 'modify_payload': { return [...items, renderPayloadEvent(data)]; } case 'tool_approval_required': { const approval = data.approval || data; return [...items, { id: newId(), kind: 'tool', title: `需要审批:${approval.tool_name || '工具调用'}`, status: 'running', body: approval.preview?.summary || '' }]; } case 'tool_approval_resolved': { return [...items, { id: newId(), kind: 'tool', title: `审批${data.decision === 'approved' ? '通过' : '拒绝'}`, status: data.decision === 'approved' ? 'success' : 'failed', body: data.reason || '' }]; } case 'task_complete': { return items; } case 'task_stopped': { return [...items, { id: newId(), kind: 'tool', title: '任务已停止', status: 'cancelled', body: data.message || '' }]; } case 'error': { return [...items, { id: newId(), kind: 'tool', title: '任务失败', status: 'failed', body: data.message || data.error || '未知错误' }]; } case 'token_update': { return items; } default: return items; } } function toolTitle(name: string, args: any, status: string): string { if (name === 'run_command') return `运行 ${args.command || ''}`.trim(); if (name === 'run_python') return '运行 Python'; if (name === 'write_file') return `Write ${args.file_path || args.path || ''} ${countWrite(args.content || '')}`.trim(); if (name === 'edit_file') return `Edited ${args.file_path || args.path || ''} ${countEdit(args)}`.trim(); if (name === 'read_file' || name === 'read_skill') return `阅读 ${args.path || args.file_path || ''}`.trim(); if (name === 'terminal_session') return `${status === 'running' ? '创建' : '终端'} ${args.session_name || args.name || ''}`.trim(); if (name === 'terminal_input') return `终端输入 ${args.command || args.input || ''}`.trim(); return name; } function toolSummaryPreview(data: any, name: string, args: any, status: string): string { const intent = data?.intent_rendered || data?.intent_full || data?.intent || args?.intent_rendered || args?.intent_full || args?.intent || ''; if (intent) return truncateSummaryPreview(firstLine(String(intent))); if (status === 'success') return '工具执行完成'; if (status === 'failed') return `${name || '工具'} 执行失败`; if (data?.status === 'preparing') return `准备调用 ${name || '工具'}...`; return `正在调用 ${name || '工具'}...`; } function firstLine(text: string): string { return text.split(/\r?\n/)[0] || ''; } function truncateSummaryPreview(text: string): string { const value = String(text || ''); return value.length > 25 ? `${value.slice(0, 25)}...` : value; } function toolInitialBody(name: string, args: any): string { if (name === 'write_file' || name === 'edit_file') return ''; return ''; } function toolResultBody(name: string, args: any, result: any): string { if (typeof result === 'string') return summarizeText(result); if (!result) return ''; if (name === 'run_command' || name === 'terminal_input' || name === 'run_python') { const exitCode = result.exit_code ?? result.returncode; const text = String(result?.output || result?.stdout || result?.stderr || '').trim(); if (exitCode !== undefined && exitCode !== 0) return text ? `exit code ${exitCode}` : `exit code ${exitCode}\n(no output)`; return text ? '' : '(no output)'; } if (name === 'read_file' || name === 'read_skill') { const content = result.content || result.text || ''; const count = String(content).split(/\r?\n/).filter(Boolean).length; const type = result.type || 'read'; if (type === 'extract') return `提取了 ${count} 行`; return `读取了 ${count} 行`; } if (name === 'write_file') return ''; if (name === 'edit_file') return ''; if (result.error) return String(result.error); return result.success === false ? '失败' : '成功'; } function toolResultLines(name: string, args: any, result: any): string[] | undefined { if (name === 'write_file') { return String(args.content || '').split('\n').map((line, idx) => `${idx + 1} +${line}`); } if (name === 'edit_file') { return editLines(args); } const text = typeof result === 'string' ? result : String(result?.output || result?.stdout || result?.content || result?.text || ''); if (!text || name === 'read_file' || name === 'read_skill') return undefined; return summarizeLines(text); } function renderPayloadEvent(data: any): TimelineItem { const path = data.path || data.file_path || ''; const lines: string[] = []; if (Array.isArray(data.old_lines)) lines.push(...data.old_lines.map((line: string, idx: number) => `${idx + 1} -${line}`)); if (Array.isArray(data.new_lines)) lines.push(...data.new_lines.map((line: string, idx: number) => `${idx + 1} +${line}`)); return { id: newId(), kind: 'tool', title: `Edited ${path}`, status: 'success', lines }; } function countWrite(content: string): string { const added = content ? content.split('\n').length : 0; return `(+${added} -0)`; } function countEdit(args: any): string { const lines = editLines(args); const removed = lines.filter((line) => line.includes(' -')).length; const added = lines.filter((line) => line.includes(' +')).length; return `(+${added} -${removed})`; } function editLines(args: any): string[] { const replacements = Array.isArray(args.replacements) ? args.replacements : []; const result: string[] = []; const emitPair = (oldText: string, newText: string) => { const oldLines = oldText.replace(/\\n/g, '\n').split('\n'); const newLines = newText.replace(/\\n/g, '\n').split('\n'); oldLines.filter((line) => line.length > 0).forEach((line, idx) => result.push(`${idx + 1} -${line}`)); newLines.filter((line) => line.length > 0).forEach((line, idx) => result.push(`${idx + 1} +${line}`)); }; if (replacements.length) { for (const rep of replacements) emitPair(String(rep.old_text || rep.old || ''), String(rep.new_text || rep.new || '')); } else { emitPair(String(args.old_string || ''), String(args.new_string || '')); } return result; } function summarizeText(text: string): string { const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0); return summarizeLines(lines.join('\n')).join('\n'); } function summarizeLines(text: string): string[] { const lines = text.split(/\r?\n/).filter((line) => line.length > 0); if (lines.length <= 5) return lines; return [lines[0]!, lines[1]!, `… +${lines.length - 4} lines`, lines[lines.length - 2]!, lines[lines.length - 1]!]; } function patchItem(items: TimelineItem[], id: string, patch: Partial): TimelineItem[] { return items.map((item) => item.id === id ? { ...item, ...patch } : item); } function newId(): string { return globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`; }