diff --git a/app/src/adapt.js b/app/src/adapt.js index 91dc877..fd1f59f 100644 --- a/app/src/adapt.js +++ b/app/src/adapt.js @@ -160,7 +160,7 @@ export function adaptActiveAgents(agentsResp, projectId) { if (projectId && a.projectId !== projectId) continue; for (const r of a.active || []) { out.push({ - id: r.runId || r.id, project: a.projectName, + id: r.runId || r.id, taskId: r.taskId, project: a.projectName, title: r.title || r.taskTitle || r.taskId || '(运行中)', kind: (r.kind || 'run').toUpperCase(), time: TIME(r.startedAt || r.at), meta: `${r.worktree || ''} · ${r.model || ''}`, diff --git a/app/src/app.jsx b/app/src/app.jsx index d5f997c..9993736 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -48,6 +48,61 @@ function NewProjectModal({ t, lang, onCreate, onClose }) { ); } +// SDK 流式消息 → 一行可读文本(助手文本 / 工具调用 / 终态 / maestro 元事件) +function summarizeMessage(m) { + if (!m || !m.type) return ''; + const content = m.message && m.message.content; + if (m.type === 'assistant' && Array.isArray(content)) { + return content.map((c) => (c.type === 'text' ? c.text : c.type === 'tool_use' ? `⚙ ${c.name}` : '')).filter(Boolean).join(' '); + } + if (m.type === 'user' && Array.isArray(content)) { + return content.some((c) => c.type === 'tool_result') ? '↩ tool result' : ''; + } + if (m.type === 'result') return `■ ${m.subtype || 'done'}`; + if (typeof m.type === 'string' && m.type.startsWith('maestro.')) return `· ${m.type}`; + return ''; +} + +// 实时流模态:EventSource 跟随某 run 的 transcript,逐条渲染 agent 输出 +function StreamModal({ agent, onClose }) { + const [lines, setLines] = React.useState([]); + const [status, setStatus] = React.useState('connecting'); + const boxRef = React.useRef(null); + React.useEffect(() => { + const es = new EventSource(`/api/tasks/${agent.taskId}/stream`); + es.addEventListener('open', () => setStatus('streaming')); + es.onmessage = (e) => { + let text = ''; + try { text = summarizeMessage(JSON.parse(e.data)); } catch { text = ''; } + if (text) setLines((ls) => [...ls.slice(-400), text]); + }; + es.addEventListener('end', () => { setStatus('done'); es.close(); }); + es.onerror = () => { setStatus('disconnected'); es.close(); }; + return () => es.close(); + }, [agent.taskId]); + React.useEffect(() => { if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight; }, [lines]); + const dot = { connecting: 'var(--amber)', streaming: 'var(--cyan)', done: 'var(--green)', disconnected: 'var(--faint)' }[status]; + return ( +