diff --git a/app/index.html b/app/index.html index f5921dd..08527c6 100644 --- a/app/index.html +++ b/app/index.html @@ -4,6 +4,11 @@ MAESTRO · 任务调度台 + + + + +
diff --git a/app/src/adapt.js b/app/src/adapt.js index 8440b54..bf4b21c 100644 --- a/app/src/adapt.js +++ b/app/src/adapt.js @@ -54,6 +54,13 @@ export function buildTaskTree(flat) { if (pid && byId.has(pid)) byId.get(pid).children.push(node); else roots.push(node); }); + // 冻结标记:父任务尚未批准拆解(plan_review/analyzing)时,子任务虽落在 speccing/ready/blocked, + // 实际被编排器闸冻结、不会领取执行。前端据此改显「待拆解确认」,避免误读成「写方案中/可执行」。 + byId.forEach((node) => { + const pid = node._raw.parentId; + const ps = pid && byId.has(pid) ? byId.get(pid)._raw.status : null; + node.frozen = ps === 'plan_review' || ps === 'analyzing'; + }); // 子节点按优先级(P0 在前)再按创建时间排序 const sortKids = (n) => { n.children.sort((a, b) => a.prio.localeCompare(b.prio) || a._raw.createdAt.localeCompare(b._raw.createdAt)); @@ -76,13 +83,15 @@ export function buildTaskTree(flat) { return { active, archived }; } -// ── 审批闸卡片 ───────────────────────────────────────── +// ── 「需你处理」卡片:审批闸(plan/spec/exec) + 需人工(attention) ──────────── export function adaptApproval(tk, projName) { - const gate = tk.status.replace('_review', ''); // plan | spec | exec + // needs_attention 不是审批闸,是卡住态——并入同区但操作不同(requeue/接管/取消,非 accept/reject) + const gate = tk.status === 'needs_attention' ? 'attention' : tk.status.replace('_review', ''); // plan | spec | exec | attention const r = tk.result || {}; let doc; if (gate === 'plan') doc = tk.plan; else if (gate === 'spec') doc = tk.spec; + else if (gate === 'attention') doc = tk.lastRunError || tk.spec || tk.plan || '(任务卡住,无错误详情——可能为执行前重评估 / 合并冲突)'; else doc = r.summary || r.diffSummary || tk.operations; // exec:优先 code review 报告(做了什么/怎么做/测试/逐条 review) const out = { id: tk.id, gate, taskId: tk.id, title: tk.title, diff --git a/app/src/app.jsx b/app/src/app.jsx index 2be15e4..c9adb73 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -300,19 +300,22 @@ export function App() { const project = projects.find((p) => p.id === currentId) || projects[0]; const projNameOf = (pid) => (projectsRaw.find((p) => p.id === pid) || {}).name; const { active: tasks, archived } = buildTaskTree(tasksRaw); + // 「需你处理」= 三个审批闸 + needs_attention(卡住待人工)。后者并入同区但操作不同。 const REVIEW = new Set(['plan_review', 'spec_review', 'exec_review']); const gates = approvalsRaw - .filter((a) => a.projectId === currentId && REVIEW.has(a.status)) + .filter((a) => a.projectId === currentId && (REVIEW.has(a.status) || a.status === 'needs_attention')) .map((a) => { const g = adaptApproval(a, projNameOf(a.projectId)); // 拆解评审:挂上实际拆出的子任务(标题/复杂度/优先级/依赖/状态),供评审弹层结构化展示 if (g.gate === 'plan') { g.subtasks = tasksRaw .filter((tk) => tk.parentId === a.id) - .map((tk) => ({ id: tk.id, title: tk.title, complexity: tk.complexity, priority: tk.priority, deps: tk.deps || [], status: tk.status })); + .map((tk) => ({ id: tk.id, title: tk.title, complexity: tk.complexity, priority: tk.priority, deps: tk.deps || [], status: tk.status, scopeFiles: tk.scopeFiles || [] })); } return g; - }); + }) + // 审批闸在前、需人工在后 + .sort((x, y) => (x.gate === 'attention' ? 1 : 0) - (y.gate === 'attention' ? 1 : 0)); const agents = adaptActiveAgents(agentsResp, currentId); const events = eventsRaw.map(adaptEvent); const quota = adaptQuota(usage); @@ -423,7 +426,7 @@ export function App() { onSync={onSync} onClose={() => setShowConfig(false)} /> : null} - + {tasks.length ? ( ) : ( diff --git a/app/src/main.jsx b/app/src/main.jsx index 04fc782..4e61a6b 100644 --- a/app/src/main.jsx +++ b/app/src/main.jsx @@ -7,6 +7,11 @@ import '@ds/_ds_bundle.js'; // 文案 i18n(真实数据由 src/api.js + src/adapt.js 提供,不再用 data.js mock) import '@ds/ui_kits/console/i18n.js'; +// 共享 Markdown 渲染组件(window.MaestroRichDoc)——须在引用它的 GateSection/TaskTree 之前 +import '@ds/ui_kits/console/RichDoc.jsx'; +// 共享 transcript 回放组件(window.MaestroTranscript)——拆解评审「拆解轨迹」等用 +import '@ds/ui_kits/console/Transcript.jsx'; + // 六个整屏 surface(保持 design/ 源文件不动,作为单一真相源) import '@ds/ui_kits/console/Sidebar.jsx'; import '@ds/ui_kits/console/EventPanel.jsx'; diff --git a/design/ui_kits/console/GateSection.jsx b/design/ui_kits/console/GateSection.jsx index ebf82d4..08aaf48 100644 --- a/design/ui_kits/console/GateSection.jsx +++ b/design/ui_kits/console/GateSection.jsx @@ -1,32 +1,4 @@ -// ── 报告渲染:把 markdown(## 章节 + - 列表 + **粗体** + `代码`)渲染成可折叠卡片 ── -function mdInline(text) { - const nodes = []; const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; - let last = 0; let m; let key = 0; - while ((m = re.exec(text))) { - if (m.index > last) nodes.push(text.slice(last, m.index)); - const tok = m[0]; - if (tok.startsWith('**')) nodes.push({tok.slice(2, -2)}); - else nodes.push({tok.slice(1, -1)}); - last = m.index + tok.length; - } - if (last < text.length) nodes.push(text.slice(last)); - return nodes; -} -function mdBody(body) { - const out = []; let key = 0; - for (const raw of body.split('\n')) { - const t = raw.trim(); - if (!t) { out.push(
); continue; } - if (/^[-*]\s/.test(t)) { - out.push(
·{mdInline(t.replace(/^[-*]\s/, ''))}
); - } else if (/^#{1,4}\s/.test(t)) { - out.push(
{mdInline(t.replace(/^#{1,4}\s/, ''))}
); - } else { - out.push(
{mdInline(t)}
); - } - } - return out; -} +// ── 报告渲染:把 markdown 按「## 章节」切成可折叠卡片,章节内容交 window.MaestroRichDoc 渲染 ── function mdSections(md) { if (!md) return []; const out = []; const lines = md.split('\n'); @@ -48,7 +20,7 @@ function ReportSectionCard({ title, body, t, defaultOpen }) { {title || '说明'}
- {open ?
{mdBody(body)}
: null} + {open ?
: null} ); } @@ -100,6 +72,85 @@ function FileDiffs({ taskId, t }) { ); } +// 单段复审报告卡片:默认折叠,标题显示复审类型 + 裁决徽章,点击展开看完整报告(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; @@ -150,9 +201,7 @@ function GatePreview({ item, t, onDecide, onClose }) { ) : null} {item.docLabel ?
{item.docLabel}
: null} - {/^##\s/m.test(item.doc || '') - ? - :
{item.doc}
} + {item.doc ? : null} {item.gate === 'exec' && rv && rv.securitySummary ? ( {secLabel(t.lblSecurity)} ) : null} @@ -179,17 +228,26 @@ function GatePreview({ item, t, onDecide, onClose }) { P{s.priority} {s.title} - {StatusChip ? : null} + {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 ? ( @@ -212,8 +270,8 @@ function GatePreview({ item, t, onDecide, onClose }) { ); } -// 审批闸区:斜纹条 + GateCard 列表(accept/reject + 驳回必填理由) -function GateSection({ approvals, onDecide, t }) { +// 「需你处理」区:审批闸(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(''); @@ -233,24 +291,36 @@ function GateSection({ approvals, onDecide, t }) { const isFolded = folded.has(a.id); return (
- toggleFold(a.id)} foldTitle={isFolded ? t.gateExpand : t.gateCollapse} - onHeaderDoubleClick={() => setPreview(a)} headerTitle={t.gateDblTip} - headerExtra={} + onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>⛶)} actions={ - - - - - - - + a.gate === 'attention' ? ( + + + {onTakeover ? : null} + + + + + ) : ( + + + + + + + + ) }> - {rejecting === a.id ? ( + {a.gate === 'attention' ? ( + + ) : rejecting === a.id ? (