// ── 报告渲染:把 markdown 按「## 章节」切成可折叠卡片,章节内容交 window.MaestroRichDoc 渲染 ── function mdSections(md) { if (!md) return []; const out = []; const lines = md.split('\n'); let cur = null; for (const line of lines) { const h = line.match(/^##\s+(.+)$/); if (h) { if (cur) out.push(cur); cur = { title: h[1].trim(), body: '' }; } else if (cur) { cur.body += (cur.body ? '\n' : '') + line; } else { if (line.trim()) { cur = { title: '', body: line }; } } // 首章节前的前言 } if (cur) out.push(cur); return out; } function ReportSectionCard({ title, body, t, defaultOpen }) { const [open, setOpen] = React.useState(defaultOpen); return (
setOpen((o) => !o)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 12px', cursor: 'pointer', background: 'var(--panel-2)', userSelect: 'none' }}> {title || '说明'}
{open ?
: null}
); } function ReportCards({ md, t }) { const secs = mdSections(md); if (!secs.length) return null; return
{secs.map((s, i) => )}
; } // 单文件 diff 渲染(+/- 行着色) function DiffBody({ diff }) { return (
      {diff.split('\n').map((ln, i) => {
        const c = (ln.startsWith('+') && !ln.startsWith('+++')) ? 'var(--green)'
          : (ln.startsWith('-') && !ln.startsWith('---')) ? 'var(--red)'
          : ln.startsWith('@@') ? 'var(--cyan)'
          : (ln.startsWith('diff --git') || ln.startsWith('index ') || ln.startsWith('+++') || ln.startsWith('---')) ? 'var(--faint)'
          : 'var(--muted)';
        return 
{ln || ' '}
; })}
); } // 改动文件列表:点击文件展开其 diff(按需向 daemon 取) function FileDiffs({ taskId, t }) { const [files, setFiles] = React.useState(null); const [openPath, setOpenPath] = React.useState(null); React.useEffect(() => { let alive = true; fetch('/api/tasks/' + taskId + '/diff').then((r) => r.json()).then((d) => { if (alive) setFiles(d.files || []); }).catch(() => { if (alive) setFiles([]); }); return () => { alive = false; }; }, [taskId]); if (!files) return
… diff
; if (!files.length) return null; return (
{files.map((f) => (
setOpenPath((p) => (p === f.path ? null : f.path))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '7px 11px', cursor: 'pointer', background: 'var(--panel-2)' }}> {f.path} {t.clickToView || ''}
{openPath === f.path ? : null}
))}
); } // 单段复审报告卡片:默认折叠,标题显示复审类型 + 裁决徽章,点击展开看完整报告(markdown)。 function ReviewReportCard({ label, verdict, report, t, first }) { const [open, setOpen] = React.useState(false); const vb = verdict === 'reject' ? { c: 'var(--red)', bd: 'var(--red-dim)', txt: '✗ ' + t.vReject } : verdict === 'approve' ? { c: 'var(--green)', bd: 'var(--green-dim)', txt: '✓ ' + t.vApprove } : null; return (
setOpen((o) => !o)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', cursor: 'pointer', userSelect: 'none', background: 'var(--panel-2)' }}> {label} {vb ? {vb.txt} : null} {open ? t.gateCollapse : t.gateExpand}
{open ?
: null}
); } // 需人工卡片:拉最近的 reviewer / security run 的「完整复审报告」(脱敏 transcript 末块=最终报告),markdown 渲染—— // 替代被截断到 200 字的 lastRunError,把驳回原因罗列清楚。无复审报告(执行崩溃/冲突/重评估等)则回退显示 lastRunError。 function ReviewReports({ taskId, fallback, t }) { const [st, setSt] = React.useState({ loading: true, reports: null }); React.useEffect(() => { let alive = true; (async () => { try { const runs = await fetch('/api/tasks/' + taskId + '/runs').then((r) => r.ok ? r.json() : []); const list = Array.isArray(runs) ? runs : []; const latest = (kind) => list.filter((r) => r.kind === kind) .slice().sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')))[0]; const out = []; for (const [kind, label] of [['reviewer', t.lblCodeReview], ['security', t.lblSecurity]]) { const run = latest(kind); if (!run) continue; const d = await fetch('/api/runs/' + run.id + '/transcript').then((r) => r.ok ? r.json() : null); const texts = ((d && d.messages) || []).filter((m) => m.type === 'text' && m.text && m.text.trim()); if (texts.length) { const report = texts[texts.length - 1].text; const vm = /VERDICT:\s*(approve|reject)/i.exec(report); out.push({ label, report, verdict: vm ? vm[1].toLowerCase() : null }); } } if (alive) setSt({ loading: false, reports: out }); } catch (e) { if (alive) setSt({ loading: false, reports: [] }); } })(); return () => { alive = false; }; }, [taskId]); if (st.loading) return
; if (st.reports && st.reports.length) { return (
{st.reports.map((r, i) => ( ))}
); } return fallback ? : null; } // 拆解评审「拆解轨迹」:默认折叠,点击展开后懒加载该任务 planner run 的完整 transcript(表格/分析+工具调用)。 function GateTranscript({ taskId, t }) { const [open, setOpen] = React.useState(false); return (
setOpen((o) => !o)} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', padding: '4px 0' }}> {t.planTranscript}
{open ? (
) : null}
); } // 全屏预览:读完整方案 + 就地裁决 function GatePreview({ item, t, onDecide, onClose }) { const { ComplexityBadge, StatusChip, Button, Textarea } = window.MaestroDesignSystem_a6a290; const [rejecting, setRejecting] = React.useState(false); const [reason, setReason] = React.useState(''); React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); if (!item) return null; // 拆解子任务序号映射(依赖用 #序号 引用) const subIdx = {}; (item.subtasks || []).forEach((s, i) => { subIdx[s.id] = i + 1; }); const rv = item.review; const vBadge = (label, v) => { const ok = v === 'approve', bad = v === 'reject'; return ( {label} {ok ? '✓ ' + t.vApprove : bad ? '✗ ' + t.vReject : t.vNone} ); }; const secLabel = (txt) =>
{txt}
; const reportBox = (txt) =>
{txt}
; return (
{t.gates[item.gate]} {item.title} {item.meta}
{item.gate === 'exec' && rv ? (
{vBadge(t.lblCodeReview, rv.verdict)} {vBadge(t.lblSecurity, rv.securityVerdict)}
) : null} {item.docLabel ?
{item.docLabel}
: null} {item.doc ? : null} {item.gate === 'exec' && rv && rv.securitySummary ? ( {secLabel(t.lblSecurity)} ) : null} {item.gate === 'exec' && rv && (rv.commits.length || rv.diffSummary) ? ( {secLabel(t.lblChanges + (rv.branch ? ' · ' + rv.branch : ''))}
{rv.commits.map((c) =>
· {c}
)} {rv.diffSummary ?
{rv.diffSummary}
: null}
) : null} {item.gate === 'plan' && item.subtasks && item.subtasks.length ? (
{t.gateSubtasks} · {item.subtasks.length}
{item.subtasks.map((s, i) => { const deps = (s.deps || []).map((d) => (subIdx[d] ? '#' + subIdx[d] : null)).filter(Boolean); return (
{i + 1} P{s.priority} {s.title} {StatusChip ? : null}
{t.dependsOn}: {deps.length ? {deps.join('、')} : t.noDeps}
{s.scopeFiles && s.scopeFiles.length ? (
{t.scopeFiles}: {s.scopeFiles.map((f) => ( {f} ))}
) : null}
); })}
) : null} {item.gate === 'plan' ? : null}
{rejecting ? (