// ── 报告渲染:把 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 ? (
) : (
)}
);
}
// 「需你处理」区:审批闸(accept/reject + 驳回必填理由)+ 需人工(requeue/接管/取消)
function GateSection({ approvals, onDecide, taskOps, onTakeover, t }) {
const { GateCard, GateStripe, Button, Textarea } = window.MaestroDesignSystem_a6a290;
const [rejecting, setRejecting] = React.useState(null);
const [reason, setReason] = React.useState('');
const [preview, setPreview] = React.useState(null);
const [sectionFolded, setSectionFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-gate') === '1');
const toggleSectionFold = () => setSectionFolded((f) => { localStorage.setItem('maestro-kit-fold-gate', f ? '0' : '1'); return !f; });
const [folded, setFolded] = React.useState(() => new Set());
const toggleFold = (id) => setFolded((prev) => {
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
});
if (approvals.length === 0) return null;
return (
{!sectionFolded ? : null}
{!sectionFolded ? approvals.map((a) => {
const isFolded = folded.has(a.id);
return (
toggleFold(a.id)}
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
onHeaderDoubleClick={a.gate === 'attention' ? undefined : () => setPreview(a)} headerTitle={a.gate === 'attention' ? null : t.gateDblTip}
headerExtra={a.gate === 'attention' ? null : ()}
actions={
a.gate === 'attention' ? (
{onTakeover ? : null}
) : (
)
}>
{a.gate === 'attention' ? (
) : rejecting === a.id ? (
) : null}
);
}) : null}
setPreview(null)} />
);
}
window.MaestroKitGateSection = GateSection;