af690701bf
- RichDoc(marked+DOMPurify+highlight.js CDN)统一渲染 gate spec/复审报告/任务树 SPEC;- Transcript 组件:拆解评审「拆解轨迹」懒加载 planner run transcript;- 「审批闸」改名「需你处理」并并入 needs_attention(requeue/接管/取消 + 完整复审报告,按代码/安全分段折叠卡片、标裁决徽章);- 子任务显 scopeFiles 文件范围 + frozen「待拆解确认」标。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
2.9 KiB
React
49 lines
2.9 KiB
React
// 历史 transcript 回放:拉某任务指定 kind 的 run 的转录(/api/tasks/:id/runs → /api/runs/:id/transcript),
|
||
// 渲染成结构化行(text / tool_use / tool_result / result)。挂 window.MaestroTranscript,供拆解评审「拆解轨迹」等处复用。
|
||
// 转录已是脱敏 + 截断的扁平 schema:{type, text?, name?, input?, isError?}(见 src/executor/transcript.ts)。
|
||
function TcLine({ m }) {
|
||
if (m.type === 'tool_use') {
|
||
return <div style={{ padding: '3px 0' }}><span style={{ color: 'var(--cyan)', fontWeight: 600 }}>⚙ {m.name}</span>{m.input ? <span style={{ color: 'var(--muted)' }}> {m.input}</span> : null}</div>;
|
||
}
|
||
if (m.type === 'tool_result') {
|
||
return <div style={{ padding: '1px 0 5px 16px', color: m.isError ? 'var(--red)' : 'var(--faint)', fontSize: 11.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>↩ {m.text || '(空)'}</div>;
|
||
}
|
||
if (m.type === 'result') return <div style={{ padding: '3px 0', color: 'var(--green)' }}>■ {m.text || 'done'}</div>;
|
||
return <div style={{ padding: '3px 0', color: 'var(--ink)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{m.text}</div>;
|
||
}
|
||
|
||
function MaestroTranscript({ taskId, kind }) {
|
||
const [state, setState] = React.useState({ loading: true, msgs: null, err: false });
|
||
React.useEffect(() => {
|
||
let alive = true;
|
||
setState({ loading: true, msgs: null, err: false });
|
||
(async () => {
|
||
try {
|
||
const runs = await fetch('/api/tasks/' + taskId + '/runs').then((r) => r.ok ? r.json() : Promise.reject(new Error('http')));
|
||
const list = Array.isArray(runs) ? runs : [];
|
||
const run = (kind ? list.filter((r) => r.kind === kind) : list)
|
||
.slice().sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')))[0] || list[0];
|
||
if (!run) { if (alive) setState({ loading: false, msgs: [], err: false }); return; }
|
||
const d = await fetch('/api/runs/' + run.id + '/transcript').then((r) => r.ok ? r.json() : Promise.reject(new Error('http')));
|
||
const msgs = Array.isArray(d) ? d : (d.messages || []);
|
||
if (alive) setState({ loading: false, msgs, err: false });
|
||
} catch (e) {
|
||
if (alive) setState({ loading: false, msgs: null, err: true });
|
||
}
|
||
})();
|
||
return () => { alive = false; };
|
||
}, [taskId, kind]);
|
||
|
||
const hint = (txt) => <div style={{ fontSize: 11.5, color: 'var(--faint)', padding: '8px 2px' }}>{txt}</div>;
|
||
if (state.loading) return hint('… 加载轨迹');
|
||
if (state.err) return hint('轨迹取不到');
|
||
if (!state.msgs || !state.msgs.length) return hint('无轨迹记录');
|
||
return (
|
||
<div style={{ maxHeight: 440, overflowY: 'auto', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.6, padding: '4px 2px' }}>
|
||
{state.msgs.map((m, i) => <TcLine key={m.seq != null ? m.seq : i} m={m} />)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
window.MaestroTranscript = MaestroTranscript;
|