Files
Agent-System-Frontend/src/components/AgentTracker.tsx
T
weichen bfaf65ae1f Converge artifact preview to one placement and one download
Why: Approved redesign phase 3: one artifact = one preview placement
(right rail by default; full-screen overlay as the only large-window form)
plus a single download button per surface. Removes the redundant
three-action chips/rows and the nested modal chrome in AgentTracker.
Evidence: npx tsc -b clean; npm test -> 38 passed. Combined runs recorded
in the Dev task card.
Impact: new store.requestPreview() is the single preview entry (Command
semantics; placement derived there); artifact chips and workspace rows
reduce to title-click = rail preview, eye = full preview, download;
docBridge card opens route through the same entry; AgentTracker drops its
inner .modal shell (OverlayHost is the only chrome). No API changes.
Refs: Agent-System-Dev task feature/ui-tokens-v3 (phase 3).
2026-09-30 19:14:31 +00:00

83 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.runId && m.runId !== runId) (subMessages[m.runId] ??= []).push(m)
}
const focus = selected ?? children[0]?.id ?? runId
const focusMsgs = focus === runId ? messagesAll.filter(m => 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>
}