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).
83 lines
4.1 KiB
TypeScript
83 lines
4.1 KiB
TypeScript
// 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>
|
||
}
|