// 历史 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
⚙ {m.name}{m.input ? {m.input} : null}
;
}
if (m.type === 'tool_result') {
return ↩ {m.text || '(空)'}
;
}
if (m.type === 'result') return ■ {m.text || 'done'}
;
return {m.text}
;
}
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) => {txt}
;
if (state.loading) return hint('… 加载轨迹');
if (state.err) return hint('轨迹取不到');
if (!state.msgs || !state.msgs.length) return hint('无轨迹记录');
return (
{state.msgs.map((m, i) => )}
);
}
window.MaestroTranscript = MaestroTranscript;