Files
Agent-System-Frontend/src/components/ProcessTimeline.tsx
T
weichen 411a79d9e2 Deliver a conversation-first progressive workbench with safe live state
Why: make material, background work, confirmations and files accessible to nontechnical users across desktop and mobile.

Evidence: Dev SELF receipts for Frontend tsc, 190 tests, knip baseline, build, nine fixture browser cases and two real HTTP/browser smoke cases using MockRunner.

Impact: existing API and permissions remain authoritative; stale events/queries and duplicate effects are guarded, while one file preview and reading state are preserved.

Refs: work-companion-ui design r0.2 and plan r1 U2-U5; UI01-UI13.
2026-10-12 16:05:13 +00:00

80 lines
3.1 KiB
TypeScript

// 执行记录按工作动作分组;展开状态由调用方控制。
import type { ReactNode } from 'react'
import { ToolBlock } from '@ui'
import type { Part } from '../contracts'
import { formatDuration, humanSize } from '../messageparts'
import { timelineEntries, toolGroupLabel, type ToolGroupItem } from '../toolkind'
export function ProcessTimeline({ parts, live, idKey, renderPart }: {
parts: Part[]
live: boolean
idKey: string
renderPart: (part: Part, liveText: boolean) => ReactNode
}) {
const rows = timelineEntries(parts, live)
return (
<div className="tl">
{rows.map((row, i) => {
// 模型内部思考不是用户工作内容,也不属于可展开的执行记录。
if (row.kind === 'thinking') return null
if (row.kind === 'text') {
return <div className="part" key={`${idKey}:tx:${i}`}>{renderPart({ type: 'text', text: row.text }, false)}</div>
}
if (row.kind === 'check') {
const text = row.passed ? '终轮检查通过' : `终轮检查未通过:${row.reason}`
return <div className="tl-check" key={`${idKey}:ck:${i}`}>{text}</div>
}
if (row.kind === 'toolblock') {
return <ToolCallBlock key={`${idKey}:tb:${i}`} item={row.item} />
}
if (row.kind === 'tools') {
const running = row.items.some(it => it.running)
return (
<div className="tl-tools" key={`${idKey}:tl:${i}`}>
<div className={`tl-tools-head${running ? ' mk-sweep' : ''}`}>{toolGroupLabel(row.toolKind, row.items.length, running)}</div>
<ul className="tl-tools-list">
{row.items.map((it, j) => (
<li key={j}>
<span className="tl-tools-name">{it.headline}</span>
{it.status === 'error' && <span className="status-error">失败</span>}
{it.summary && <span className="tl-tools-sum">{it.summary}</span>}
</li>
))}
</ul>
</div>
)
}
return <div className="part" key={`${idKey}:ot:${i}`}>{renderPart(row.part, false)}</div>
})}
</div>
)
}
function sizeLabel(n: number): string {
if (n < 1024) return `${n} 字符`
return humanSize(n)
}
function ToolCallBlock({ item }: { item: ToolGroupItem }) {
const composing = item.phase === 'composing'
const dur = !item.running ? formatDuration(item.durationMs) : ''
const detail = composing ? undefined : (dur ? `${item.headline} · ${dur}` : item.headline)
return (
<div className="tl-toolblock">
<ToolBlock
toolId={item.id}
name={item.name}
detail={detail}
running={item.running}
status={item.status}
statusText={composing ? `编写中 · ${sizeLabel(item.arguments?.length ?? 0)}` : undefined}
statusDetail={item.summary}
autoCollapseOnComplete
>
{item.arguments && <pre className="tool-preview">{item.arguments}</pre>}
{item.output && <pre className="tool-preview">{item.output}</pre>}
</ToolBlock>
</div>
)
}