Files
Agent-System-Frontend/src/components/AgentTracker.tsx
T
weichenandCursor e4ca2acfa6 Show a run as active as soon as the send returns
Why: Stamping the user message with the run id made the live bubble look settled, and the composer stayed on 发送 until the next reconcile.
Evidence: npx tsc -b passed. vitest 75, including hasSettledReply and mergeStartedRun. On a mock server the button became 停止/排队 within 250ms and the reply grew while the run was open.
Impact: Composer, live bubble, and agent tracker only. A stored user message no longer hides the stream. Reopening replays from resumeSeq when the server sends it.
Refs: Dev fix/live-run-visibility
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 15:21:28 +00:00

85 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Sub-agent 追踪弹窗(4.4):任务树、状态、中间过程(子代理消息树)与结果。
import { useEffect, useState } from 'react'
import { api } from '../api'
import { store, useStore } from '../store'
import { OverlayHost } from '@ui'
import type { Message, Run, Usage } from '../contracts'
export function AgentTracker() {
const runId = useStore(s => s.ui.trackerRunId)!
const messagesAll = useStore(s => s.messages)
const [root, setRoot] = useState<Run | null>(null)
const [children, setChildren] = useState<Run[]>([])
const [selected, setSelected] = useState<string | null>(null)
useEffect(() => {
api.runDetail(runId).then(d => { setRoot(d.run); setChildren(d.children ?? []) }).catch(() => undefined)
}, [runId])
// 中间过程:该会话消息树中归属子代理 run 的消息(run_id 过滤)。
const subMessages: Record<string, Message[]> = {}
for (const m of messagesAll) {
if (m.role === 'assistant' && m.runId && m.runId !== runId) (subMessages[m.runId] ??= []).push(m)
}
const focus = selected ?? children[0]?.id ?? runId
const focusMsgs = focus === runId
? messagesAll.filter(m => m.role === 'assistant' && m.runId === runId)
: subMessages[focus] ?? []
return (
// 阶段③ 收敛:只保留 OverlayHost 一层壳(此前在其内又嵌 .modal 双层窗框)。
<OverlayHost title="代理追踪" icon="🤖" onClose={() => store.setUI({ trackerRunId: null })}>
<div className="subagent-tree">
{!root && <div>加载中…</div>}
{root && (
<>
<div className="subagent-node">
<div className="head">
<span className={`status-dot status-${root.status}`} />
<strong>{root.kind === 'subagent' ? root.agentName || '子代理' : '主任务'}</strong>
<span style={{ color: 'var(--text-faint)' }}>{root.status}{root.phase ? ` · ${root.phase}` : ''}</span>
<span className="spacer" style={{ flex: 1 }} />
<UsageBadge usage={root.usage} />
</div>
{children.length > 0 && (
<div>
{children.map(c => (
<div key={c.id} className="agent-row" style={{ background: focus === c.id ? 'var(--accent-soft)' : undefined, borderRadius: 6 }}
onClick={() => setSelected(c.id)}>
<span className={`status-dot status-${c.status}`} />
<span style={{ flex: 1 }}>🤖 {c.agentName || c.id}</span>
<span style={{ color: 'var(--text-faint)', fontSize: 'var(--fs-ui-xs)' }}>{c.status}</span>
</div>
))}
</div>
)}
</div>
<h5 style={{ margin: '12px 0 6px' }}>中间过程({focus === runId ? '主任务' : '子代理'})</h5>
<div style={{ background: 'var(--bg)', border: '1px solid var(--border)', borderRadius: 8, padding: 12, maxHeight: '40vh', overflowY: 'auto' }}>
{focusMsgs.length === 0 && <div style={{ color: 'var(--text-faint)', fontSize: 'var(--fs-ui-sm)' }}>(无中间过程消息)</div>}
{focusMsgs.map(m => (
<div key={m.id} className="part">
{m.parts.map((p, i) => (
<div key={i} style={{ whiteSpace: 'pre-wrap', fontSize: 'var(--fs-ui-caption)', lineHeight: 1.7 }}>
{p.type === 'text' && p.text}
{p.type === 'thinking' && <span style={{ color: 'var(--text-faint)' }}>{p.text}</span>}
{p.type === 'tool_call' && <span className="tool-chip">🛠 {p.toolCall?.name}</span>}
</div>
))}
</div>
))}
</div>
</>
)}
</div>
</OverlayHost>
)
}
function UsageBadge({ usage }: { usage?: Usage | null }) {
if (!usage) return null
return <span className="effect-badge" title="token 用量与时长">
{usage.totalTokens > 0 ? `${usage.totalTokens} tok · ` : ''}{(usage.durationMs / 1000).toFixed(1)}s{usage.toolCalls > 0 ? ` · ${usage.toolCalls} 工具` : ''}
</span>
}