feat(web): Markdown 渲染器 + 拆解轨迹 + 需你处理区 + 复审报告折叠卡片
- 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>
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>MAESTRO · 任务调度台</title>
|
||||
<!-- Markdown 渲染(方案/复审/任务树 SPEC 用,window 全局,先于 module 脚本加载)-->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/styles/github-dark.min.css" integrity="sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js" integrity="sha384-/TQbtLCAerC3jgaim+N78RZSDYV7ryeoBCVqTuzRrFec2akfBkHS7ACQ3PQhvMVi" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+11
-2
@@ -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,
|
||||
|
||||
+7
-4
@@ -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}
|
||||
<window.MaestroKitAgentSection agents={agents} quota={quota} t={t} onOpenStream={setStreamAgent} />
|
||||
<window.MaestroKitGateSection approvals={gates} onDecide={decide} t={t} />
|
||||
<window.MaestroKitGateSection approvals={gates} onDecide={decide} taskOps={taskOps} onTakeover={takeover} t={t} />
|
||||
{tasks.length ? (
|
||||
<window.MaestroKitTaskSection tasks={tasks} onToast={toast} onCreate={createTask} onTakeover={takeover} taskOps={taskOps} t={t} />
|
||||
) : (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(<b key={key++} style={{ color: 'var(--ink)' }}>{tok.slice(2, -2)}</b>);
|
||||
else nodes.push(<code key={key++} style={{ fontFamily: 'var(--mono)', fontSize: '.92em', color: 'var(--cyan)', background: 'var(--panel-2)', borderRadius: 3, padding: '1px 5px' }}>{tok.slice(1, -1)}</code>);
|
||||
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(<div key={key++} style={{ height: 6 }} />); continue; }
|
||||
if (/^[-*]\s/.test(t)) {
|
||||
out.push(<div key={key++} style={{ display: 'flex', gap: 8, padding: '2px 0 2px 2px' }}><span style={{ flex: 'none', color: 'var(--green)' }}>·</span><span>{mdInline(t.replace(/^[-*]\s/, ''))}</span></div>);
|
||||
} else if (/^#{1,4}\s/.test(t)) {
|
||||
out.push(<div key={key++} style={{ fontWeight: 700, color: 'var(--muted)', margin: '6px 0 2px' }}>{mdInline(t.replace(/^#{1,4}\s/, ''))}</div>);
|
||||
} else {
|
||||
out.push(<div key={key++} style={{ padding: '1px 0' }}>{mdInline(t)}</div>);
|
||||
}
|
||||
}
|
||||
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 }) {
|
||||
<span style={{ flex: 'none', color: 'var(--green)', fontSize: 9, transition: 'transform .12s', transform: open ? 'none' : 'rotate(-90deg)' }}>▼</span>
|
||||
<span style={{ flex: 1, fontSize: 12.5, fontWeight: 700, color: 'var(--ink)', letterSpacing: '.02em' }}>{title || '说明'}</span>
|
||||
</div>
|
||||
{open ? <div style={{ padding: '11px 14px', fontSize: 12.5, lineHeight: 1.7, color: 'var(--ink)', wordBreak: 'break-word' }}>{mdBody(body)}</div> : null}
|
||||
{open ? <div style={{ padding: '11px 14px' }}><window.MaestroRichDoc md={body} /></div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{ marginTop: first ? 0 : 8, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 6, overflow: 'hidden' }}>
|
||||
<div onClick={() => setOpen((o) => !o)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', cursor: 'pointer', userSelect: 'none', background: 'var(--panel-2)' }}>
|
||||
<span style={{ color: 'var(--green)', fontSize: 9, transition: 'transform .12s', transform: open ? 'none' : 'rotate(-90deg)' }}>▼</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '.14em', color: 'var(--muted)' }}>{label}</span>
|
||||
{vb ? <span style={{ fontSize: 10, fontWeight: 700, color: vb.c, border: '1px solid ' + vb.bd, borderRadius: 3, padding: '0 6px' }}>{vb.txt}</span> : null}
|
||||
<span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--faint)' }}>{open ? t.gateCollapse : t.gateExpand}</span>
|
||||
</div>
|
||||
{open ? <div style={{ padding: '11px 13px' }}><window.MaestroRichDoc md={report} /></div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 需人工卡片:拉最近的 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 <div style={{ fontSize: 11.5, color: 'var(--faint)', padding: '4px 2px' }}>…</div>;
|
||||
if (st.reports && st.reports.length) {
|
||||
return (
|
||||
<div>
|
||||
{st.reports.map((r, i) => (
|
||||
<ReviewReportCard key={i} label={r.label} verdict={r.verdict} report={r.report} t={t} first={i === 0} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return fallback ? <window.MaestroRichDoc md={fallback} /> : null;
|
||||
}
|
||||
|
||||
// 拆解评审「拆解轨迹」:默认折叠,点击展开后懒加载该任务 planner run 的完整 transcript(表格/分析+工具调用)。
|
||||
function GateTranscript({ taskId, t }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
return (
|
||||
<div style={{ marginTop: 18 }}>
|
||||
<div onClick={() => setOpen((o) => !o)} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, cursor: 'pointer', userSelect: 'none', padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--green)', fontSize: 9, transition: 'transform .12s', transform: open ? 'none' : 'rotate(-90deg)' }}>▼</span>
|
||||
<span style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--muted)', letterSpacing: '.18em' }}>{t.planTranscript}</span>
|
||||
</div>
|
||||
{open ? (
|
||||
<div style={{ marginTop: 8, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 6, padding: '8px 12px' }}>
|
||||
<window.MaestroTranscript taskId={taskId} kind="planner" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 全屏预览:读完整方案 + 就地裁决
|
||||
function GatePreview({ item, t, onDecide, onClose }) {
|
||||
const { ComplexityBadge, StatusChip, Button, Textarea } = window.MaestroDesignSystem_a6a290;
|
||||
@@ -150,9 +201,7 @@ function GatePreview({ item, t, onDecide, onClose }) {
|
||||
</div>
|
||||
) : null}
|
||||
{item.docLabel ? <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{item.docLabel}</div> : null}
|
||||
{/^##\s/m.test(item.doc || '')
|
||||
? <ReportCards md={item.doc} t={t} />
|
||||
: <div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '12px 16px', fontSize: 13.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{item.doc}</div>}
|
||||
{item.doc ? <ReportCards md={item.doc} t={t} /> : null}
|
||||
{item.gate === 'exec' && rv && rv.securitySummary ? (
|
||||
<React.Fragment>{secLabel(t.lblSecurity)}<ReportCards md={rv.securitySummary} t={t} /></React.Fragment>
|
||||
) : null}
|
||||
@@ -179,17 +228,26 @@ function GatePreview({ item, t, onDecide, onClose }) {
|
||||
<ComplexityBadge complexity={s.complexity} />
|
||||
<span style={{ flex: 'none', fontSize: 10, fontWeight: 700, color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 3, padding: '1px 6px' }}>P{s.priority}</span>
|
||||
<span style={{ flex: 1, fontSize: 13.5, fontWeight: 600, color: 'var(--ink)', minWidth: 120 }}>{s.title}</span>
|
||||
{StatusChip ? <StatusChip status={s.status} label={t.status[s.status]} /> : null}
|
||||
{StatusChip ? <StatusChip status="paused" label={t.frozenPending} /> : null}
|
||||
</div>
|
||||
<div style={{ marginTop: 7, paddingLeft: 31, fontSize: 11.5, color: 'var(--faint)' }}>
|
||||
{t.dependsOn}: {deps.length ? <span style={{ color: 'var(--amber)', fontWeight: 600 }}>{deps.join('、')}</span> : t.noDeps}
|
||||
</div>
|
||||
{s.scopeFiles && s.scopeFiles.length ? (
|
||||
<div style={{ marginTop: 5, paddingLeft: 31, display: 'flex', flexWrap: 'wrap', gap: 5, alignItems: 'center' }}>
|
||||
<span style={{ flex: 'none', fontSize: 11.5, color: 'var(--faint)' }}>{t.scopeFiles}:</span>
|
||||
{s.scopeFiles.map((f) => (
|
||||
<span key={f} style={{ fontSize: 10.5, fontFamily: 'var(--mono)', color: 'var(--cyan)', background: 'var(--panel-2)', border: '1px solid var(--line-soft)', borderRadius: 3, padding: '1px 6px' }}>{f}</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{item.gate === 'plan' ? <GateTranscript taskId={item.taskId} t={t} /> : null}
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderTop: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
||||
{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 (
|
||||
<div key={a.id} style={{ marginBottom: 12 }}>
|
||||
<GateCard gate={a.gate} kindLabel={t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.doc}
|
||||
<GateCard gate={a.gate} kindLabel={a.gate === 'attention' ? t.gateNeedsHuman : t.gates[a.gate]} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.gate === 'attention' ? undefined : a.doc}
|
||||
collapsed={isFolded} foldable onToggleFold={() => toggleFold(a.id)}
|
||||
foldTitle={isFolded ? t.gateExpand : t.gateCollapse}
|
||||
onHeaderDoubleClick={() => setPreview(a)} headerTitle={t.gateDblTip}
|
||||
headerExtra={<button type="button" title={t.fullRead} onClick={() => setPreview(a)}
|
||||
onHeaderDoubleClick={a.gate === 'attention' ? undefined : () => setPreview(a)} headerTitle={a.gate === 'attention' ? null : t.gateDblTip}
|
||||
headerExtra={a.gate === 'attention' ? null : (<button type="button" title={t.fullRead} onClick={() => setPreview(a)}
|
||||
style={{ fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1, cursor: 'pointer', background: 'transparent', color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 'var(--radius-sm,4px)', padding: '3px 7px' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--violet)'; e.currentTarget.style.borderColor = 'var(--violet-dim)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>⛶</button>}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>⛶</button>)}
|
||||
actions={
|
||||
<React.Fragment>
|
||||
<Button variant="accept" onClick={() => onDecide(a, 'accept')}>{t.accept}</Button>
|
||||
<Button variant="reject" onClick={() => setRejecting(rejecting === a.id ? null : a.id)}>{t.reject}</Button>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<Button variant="ghost" size="xs" title={t.fullRead} onClick={() => setPreview(a)}>⛶ {t.fullRead}</Button>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
a.gate === 'attention' ? (
|
||||
<React.Fragment>
|
||||
<Button variant="accept" onClick={() => taskOps && taskOps.requeue(a.taskId)}>{t.gRequeue}</Button>
|
||||
{onTakeover ? <Button variant="ghost" onClick={() => onTakeover({ id: a.taskId, title: a.title })}>{t.gTakeover}</Button> : null}
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<Button variant="reject" size="xs" onClick={() => taskOps && taskOps.cancel(a.taskId)}>{t.gCancelTask}</Button>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Button variant="accept" onClick={() => onDecide(a, 'accept')}>{t.accept}</Button>
|
||||
<Button variant="reject" onClick={() => setRejecting(rejecting === a.id ? null : a.id)}>{t.reject}</Button>
|
||||
<span style={{ marginLeft: 'auto' }}>
|
||||
<Button variant="ghost" size="xs" title={t.fullRead} onClick={() => setPreview(a)}>⛶ {t.fullRead}</Button>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
)
|
||||
}>
|
||||
{rejecting === a.id ? (
|
||||
{a.gate === 'attention' ? (
|
||||
<ReviewReports taskId={a.taskId} fallback={a.doc} t={t} />
|
||||
) : rejecting === a.id ? (
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', marginTop: 10 }}>
|
||||
<Textarea danger placeholder={t.rejectPlaceholder} minHeight={56} value={reason} onChange={setReason} />
|
||||
<Button variant="reject" disabled={!reason.trim()}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 共享 Markdown 渲染:marked 解析 + DOMPurify 净化 + highlight.js 代码高亮。
|
||||
// 挂 window.MaestroRichDoc,供 GateSection / TaskTree 等在 vite app 与 standalone demo 两环境共用。
|
||||
// 依赖 window.marked / window.DOMPurify(必需,CDN 全局)+ window.hljs(可选,缺则代码块不着色)。
|
||||
function ensureRichDocCss() {
|
||||
if (document.getElementById('maestro-richdoc-css')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'maestro-richdoc-css';
|
||||
s.textContent = `
|
||||
.m-richdoc { font-size: 12.8px; line-height: 1.7; color: var(--ink); word-break: break-word; }
|
||||
.m-richdoc > :first-child { margin-top: 0; }
|
||||
.m-richdoc > :last-child { margin-bottom: 0; }
|
||||
.m-richdoc h1, .m-richdoc h2, .m-richdoc h3, .m-richdoc h4 { font-weight: 700; line-height: 1.3; margin: 16px 0 8px; letter-spacing: .01em; }
|
||||
.m-richdoc h1 { font-size: 1.32em; color: var(--ink); border-bottom: 1px solid var(--line-soft); padding-bottom: 5px; }
|
||||
.m-richdoc h2 { font-size: 1.16em; color: var(--green); }
|
||||
.m-richdoc h3 { font-size: 1.05em; color: var(--cyan); }
|
||||
.m-richdoc h4 { font-size: 1em; color: var(--muted); }
|
||||
.m-richdoc p { margin: 7px 0; }
|
||||
.m-richdoc ul, .m-richdoc ol { margin: 7px 0; padding-left: 22px; }
|
||||
.m-richdoc li { margin: 3px 0; }
|
||||
.m-richdoc li::marker { color: var(--green); }
|
||||
.m-richdoc a { color: var(--cyan); text-decoration: none; border-bottom: 1px solid var(--cyan-dim); }
|
||||
.m-richdoc a:hover { border-bottom-color: var(--cyan); }
|
||||
.m-richdoc strong { color: var(--ink); font-weight: 700; }
|
||||
.m-richdoc em { color: var(--muted); }
|
||||
.m-richdoc code { font-family: var(--mono); font-size: .9em; color: var(--cyan); background: var(--panel-2); border-radius: 3px; padding: 1.5px 5px; }
|
||||
.m-richdoc pre { background: var(--bg-deep); border: 1px solid var(--line-soft); border-radius: 5px; padding: 11px 13px; overflow-x: auto; margin: 9px 0; }
|
||||
.m-richdoc pre code { display: block; font-family: var(--mono); font-size: 11.8px; line-height: 1.55; color: var(--ink); background: none; border: 0; padding: 0; }
|
||||
.m-richdoc blockquote { margin: 9px 0; padding: 2px 13px; border-left: 3px solid var(--line); color: var(--muted); }
|
||||
.m-richdoc table { border-collapse: collapse; margin: 9px 0; font-size: .95em; display: block; overflow-x: auto; }
|
||||
.m-richdoc th, .m-richdoc td { border: 1px solid var(--line); padding: 5px 10px; text-align: left; }
|
||||
.m-richdoc th { background: var(--panel-2); color: var(--ink); font-weight: 700; }
|
||||
.m-richdoc hr { border: 0; border-top: 1px solid var(--line-soft); margin: 14px 0; }
|
||||
.m-richdoc img { max-width: 100%; }
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function MaestroRichDoc({ md }) {
|
||||
const ref = React.useRef(null);
|
||||
ensureRichDocCss();
|
||||
const ready = typeof window !== 'undefined' && !!window.marked && !!window.DOMPurify;
|
||||
const html = React.useMemo(() => {
|
||||
if (!ready || !md) return null;
|
||||
try {
|
||||
const raw = window.marked.parse(String(md), { gfm: true, breaks: false });
|
||||
return window.DOMPurify.sanitize(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [md, ready]);
|
||||
React.useEffect(() => {
|
||||
if (html && ref.current && window.hljs) {
|
||||
ref.current.querySelectorAll('pre code').forEach((el) => {
|
||||
try { window.hljs.highlightElement(el); } catch (e) { /* noop */ }
|
||||
});
|
||||
}
|
||||
}, [html]);
|
||||
// 兜底:库未就绪或解析失败 → 原文 pre-wrap,永不崩
|
||||
if (html == null) {
|
||||
return <pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.6, color: 'var(--ink)' }}>{md || ''}</pre>;
|
||||
}
|
||||
return <div ref={ref} className="m-richdoc" dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
|
||||
window.MaestroRichDoc = MaestroRichDoc;
|
||||
@@ -234,7 +234,9 @@ function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleO
|
||||
) : null}
|
||||
<span style={{ fontSize: 10.5, color: task.prio === 'P0' ? 'var(--amber)' : 'var(--faint)', flex: 'none' }}>{task.prio}</span>
|
||||
<CplxPicker task={task} cplx={cplxOf(task)} onChange={onChangeCplx} />
|
||||
<StatusChip status={task.status} label={t.status[task.status]} />
|
||||
{task.frozen
|
||||
? <StatusChip status="paused" label={t.frozenPending} />
|
||||
: <StatusChip status={task.status} label={t.status[task.status]} />}
|
||||
</div>
|
||||
{expanded ? <TaskDetail task={task} t={t} byId={byId} onJump={onJump} onTakeover={onTakeover} taskOps={taskOps} /> : null}
|
||||
{kids.length && open ? (
|
||||
@@ -315,7 +317,7 @@ function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
|
||||
{label ? (
|
||||
<div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{label}</div>
|
||||
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)', borderRadius: 4 }}>{text}</pre>
|
||||
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', padding: '10px 14px', borderRadius: 4 }}><window.MaestroRichDoc md={text} /></div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 6, color: 'var(--faint)', fontStyle: 'italic', fontSize: 12 }}>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 历史 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;
|
||||
@@ -35,7 +35,7 @@ window.MAESTRO_I18N = {
|
||||
fgroups: { todo: '待办', doing: '进行中', gate: '待审批', bad: '异常', hold: '挂起', done: '完成' },
|
||||
fStatus: '状态', fAssignee: '执行者', assignees: { agent: '智能体', human: '人工' },
|
||||
fullRead: '全屏阅读', depsLabel: 'DEPS · 依赖(全部完成才可执行)', depDone: '完成', depWait: '等待',
|
||||
gateSection: '审批闸', accept: '✓ 通过', reject: '✕ 驳回', confirmReject: '确认驳回',
|
||||
gateSection: '需你处理', gateNeedsHuman: '需人工', gRequeue: '↻ 重投', gTakeover: '⌨ 接管', gCancelTask: '⊘ 取消', accept: '✓ 通过', reject: '✕ 驳回', confirmReject: '确认驳回',
|
||||
rejectPlaceholder: '改进意见(必填)',
|
||||
taskSection: '任务树', newTask: '+ 新建任务', titleLabel: '标题', titlePlaceholder: '要做什么',
|
||||
complexity: '复杂度', cplxAuto: '智能', parentTask: '父任务', topLevel: '(顶层)', priority: '优先级',
|
||||
@@ -49,7 +49,7 @@ window.MAESTRO_I18N = {
|
||||
gatePassed: ' 通过',
|
||||
status: { init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解', decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案', ready: '可执行', blocked: '被依赖阻塞', queued: '排队中', executing: '执行中', exec_review: '待审/合', failed: '失败', needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消' },
|
||||
gates: { plan: '拆解评审', spec: '方案评审', exec: '结果评审' },
|
||||
gateSubtasks: '拆解出的子任务', noDeps: '无依赖', dependsOn: '依赖',
|
||||
gateSubtasks: '拆解出的子任务', frozenPending: '待拆解确认', scopeFiles: '文件范围', planTranscript: '拆解轨迹(agent 执行记录)', noDeps: '无依赖', dependsOn: '依赖',
|
||||
lblCodeReview: '代码复审', lblSecurity: '安全审计', lblChanges: '改动', vApprove: '通过', vReject: '驳回', vNone: '无结论',
|
||||
eventTypes: { 'task.created': '任务创建', 'task.updated': '任务更新', 'status.changed': '状态变更', 'approval.requested': '请求审批', 'approval.granted': '审批通过', 'approval.rejected': '审批驳回', 'run.started': '运行开始', 'run.finished': '运行结束', 'project.synced': 'todo 同步' },
|
||||
},
|
||||
@@ -85,7 +85,7 @@ window.MAESTRO_I18N = {
|
||||
fgroups: { todo: 'To do', doing: 'In progress', gate: 'Pending review', bad: 'Issues', hold: 'On hold', done: 'Done' },
|
||||
fStatus: 'Status', fAssignee: 'Assignee', assignees: { agent: 'Agent', human: 'Human' },
|
||||
fullRead: 'Read fullscreen', depsLabel: 'DEPS (all must complete to run)', depDone: 'done', depWait: 'waiting',
|
||||
gateSection: 'Approval gates', accept: '✓ Accept', reject: '✕ Reject', confirmReject: 'Confirm reject',
|
||||
gateSection: 'Needs you', gateNeedsHuman: 'Needs human', gRequeue: '↻ Requeue', gTakeover: '⌨ Take over', gCancelTask: '⊘ Cancel', accept: '✓ Accept', reject: '✕ Reject', confirmReject: 'Confirm reject',
|
||||
rejectPlaceholder: 'Improvement feedback (required)',
|
||||
taskSection: 'Task tree', newTask: '+ New task', titleLabel: 'Title', titlePlaceholder: 'What needs doing',
|
||||
complexity: 'Complexity', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(top level)', priority: 'Priority',
|
||||
@@ -99,7 +99,7 @@ window.MAESTRO_I18N = {
|
||||
gatePassed: ' approved',
|
||||
status: { init: 'New', analyzing: 'Analyzing', plan_review: 'Plan review', decomposed: 'Decomposed', speccing: 'Drafting spec', spec_review: 'Spec review', ready: 'Ready', blocked: 'Blocked', queued: 'Queued', executing: 'Executing', exec_review: 'Review & merge', failed: 'Failed', needs_attention: 'Needs attention', done: 'Done', paused: 'Paused', cancelled: 'Cancelled' },
|
||||
gates: { plan: 'PLAN REVIEW', spec: 'SPEC REVIEW', exec: 'EXEC REVIEW' },
|
||||
gateSubtasks: 'Decomposed subtasks', noDeps: 'no deps', dependsOn: 'deps',
|
||||
gateSubtasks: 'Decomposed subtasks', frozenPending: 'awaiting approval', scopeFiles: 'files', planTranscript: 'Decompose trace', noDeps: 'no deps', dependsOn: 'deps',
|
||||
lblCodeReview: 'Code review', lblSecurity: 'Security', lblChanges: 'Changes', vApprove: 'Approve', vReject: 'Reject', vNone: 'No verdict',
|
||||
eventTypes: { 'task.created': 'Task created', 'task.updated': 'Task updated', 'status.changed': 'Status changed', 'approval.requested': 'Approval requested', 'approval.granted': 'Approval granted', 'approval.rejected': 'Approval rejected', 'run.started': 'Run started', 'run.finished': 'Run finished', 'project.synced': 'Todo synced' },
|
||||
},
|
||||
@@ -135,7 +135,7 @@ window.MAESTRO_I18N = {
|
||||
fgroups: { todo: 'Pendientes', doing: 'En curso', gate: 'Por revisar', bad: 'Incidencias', hold: 'En pausa', done: 'Hechas' },
|
||||
fStatus: 'Estado', fAssignee: 'Ejecutor', assignees: { agent: 'Agente', human: 'Humano' },
|
||||
fullRead: 'Pantalla completa', depsLabel: 'DEPS (todas deben completarse)', depDone: 'hecha', depWait: 'esperando',
|
||||
gateSection: 'Puertas de aprobación', accept: '✓ Aceptar', reject: '✕ Rechazar', confirmReject: 'Confirmar rechazo',
|
||||
gateSection: 'Requiere acción', gateNeedsHuman: 'Manual', gRequeue: '↻ Reencolar', gTakeover: '⌨ Tomar', gCancelTask: '⊘ Cancelar', accept: '✓ Aceptar', reject: '✕ Rechazar', confirmReject: 'Confirmar rechazo',
|
||||
rejectPlaceholder: 'Comentario de mejora (obligatorio)',
|
||||
taskSection: 'Árbol de tareas', newTask: '+ Nueva tarea', titleLabel: 'Título', titlePlaceholder: 'Qué hay que hacer',
|
||||
complexity: 'Complejidad', cplxAuto: 'AUTO', parentTask: 'Padre', topLevel: '(raíz)', priority: 'Prioridad',
|
||||
@@ -149,7 +149,7 @@ window.MAESTRO_I18N = {
|
||||
gatePassed: ' aprobada',
|
||||
status: { init: 'Nueva', analyzing: 'Analizando', plan_review: 'Revisión de plan', decomposed: 'Descompuesta', speccing: 'Redactando spec', spec_review: 'Revisión de spec', ready: 'Lista', blocked: 'Bloqueada', queued: 'En cola', executing: 'Ejecutando', exec_review: 'Revisar y fusionar', failed: 'Fallida', needs_attention: 'Requiere atención', done: 'Hecha', paused: 'Pausada', cancelled: 'Cancelada' },
|
||||
gates: { plan: 'REVISIÓN DE PLAN', spec: 'REVISIÓN DE SPEC', exec: 'REVISIÓN DE RESULTADO' },
|
||||
gateSubtasks: 'Subtareas', noDeps: 'sin deps', dependsOn: 'deps',
|
||||
gateSubtasks: 'Subtareas', frozenPending: 'pendiente aprob.', scopeFiles: 'archivos', planTranscript: 'Traza de descomposición', noDeps: 'sin deps', dependsOn: 'deps',
|
||||
lblCodeReview: 'Revisión', lblSecurity: 'Seguridad', lblChanges: 'Cambios', vApprove: 'Aprobar', vReject: 'Rechazar', vNone: 'Sin veredicto',
|
||||
eventTypes: { 'task.created': 'Tarea creada', 'task.updated': 'Tarea actualizada', 'status.changed': 'Cambio de estado', 'approval.requested': 'Aprobación solicitada', 'approval.granted': 'Aprobación concedida', 'approval.rejected': 'Aprobación rechazada', 'run.started': 'Run iniciado', 'run.finished': 'Run terminado', 'project.synced': 'Todo sincronizado' },
|
||||
},
|
||||
@@ -185,7 +185,7 @@ window.MAESTRO_I18N = {
|
||||
fgroups: { todo: '未着手', doing: '進行中', gate: '承認待ち', bad: '異常', hold: '保留', done: '完了' },
|
||||
fStatus: 'ステータス', fAssignee: '担当', assignees: { agent: 'エージェント', human: '人手' },
|
||||
fullRead: '全画面で読む', depsLabel: 'DEPS · 依存(全完了で実行可)', depDone: '完了', depWait: '待機',
|
||||
gateSection: '承認ゲート', accept: '✓ 承認', reject: '✕ 却下', confirmReject: '却下を確定',
|
||||
gateSection: '要対応', gateNeedsHuman: '要人手', gRequeue: '↻ 再投入', gTakeover: '⌨ 引継ぎ', gCancelTask: '⊘ 取消', accept: '✓ 承認', reject: '✕ 却下', confirmReject: '却下を確定',
|
||||
rejectPlaceholder: '改善フィードバック(必須)',
|
||||
taskSection: 'タスクツリー', newTask: '+ 新規タスク', titleLabel: 'タイトル', titlePlaceholder: '何をしますか',
|
||||
complexity: '複雑度', cplxAuto: '智能', parentTask: '親タスク', topLevel: '(トップ)', priority: '優先度',
|
||||
@@ -199,7 +199,7 @@ window.MAESTRO_I18N = {
|
||||
gatePassed: ' 承認',
|
||||
status: { init: '新規', analyzing: '分析・分解中', plan_review: '分解確認待ち', decomposed: '分解済み', speccing: '方針作成中', spec_review: '方針確認待ち', ready: '実行可能', blocked: '依存ブロック', queued: 'キュー待ち', executing: '実行中', exec_review: 'レビュー/マージ', failed: '失敗', needs_attention: '要対応', done: '完了', paused: '一時停止', cancelled: 'キャンセル' },
|
||||
gates: { plan: '分解レビュー', spec: '方針レビュー', exec: '結果レビュー' },
|
||||
gateSubtasks: '分解されたサブタスク', noDeps: '依存なし', dependsOn: '依存',
|
||||
gateSubtasks: '分解されたサブタスク', frozenPending: '承認待ち', scopeFiles: 'ファイル範囲', planTranscript: '分解トレース', noDeps: '依存なし', dependsOn: '依存',
|
||||
lblCodeReview: 'コードレビュー', lblSecurity: 'セキュリティ', lblChanges: '変更', vApprove: '承認', vReject: '却下', vNone: '判定なし',
|
||||
eventTypes: { 'task.created': 'タスク作成', 'task.updated': 'タスク更新', 'status.changed': 'ステータス変更', 'approval.requested': '承認リクエスト', 'approval.granted': '承認', 'approval.rejected': '却下', 'run.started': 'run 開始', 'run.finished': 'run 終了', 'project.synced': 'todo 同期' },
|
||||
},
|
||||
@@ -235,7 +235,7 @@ window.MAESTRO_I18N = {
|
||||
fgroups: { todo: 'À faire', doing: 'En cours', gate: 'À approuver', bad: 'Anomalies', hold: 'En attente', done: 'Terminées' },
|
||||
fStatus: 'Statut', fAssignee: 'Exécutant', assignees: { agent: 'Agent', human: 'Humain' },
|
||||
fullRead: 'Plein écran', depsLabel: 'DEPS (toutes requises)', depDone: 'terminée', depWait: 'en attente',
|
||||
gateSection: "Portes d'approbation", accept: '✓ Accepter', reject: '✕ Rejeter', confirmReject: 'Confirmer le rejet',
|
||||
gateSection: 'À traiter', gateNeedsHuman: 'Manuel', gRequeue: '↻ Relancer', gTakeover: '⌨ Reprendre', gCancelTask: '⊘ Annuler', accept: '✓ Accepter', reject: '✕ Rejeter', confirmReject: 'Confirmer le rejet',
|
||||
rejectPlaceholder: "Retour d'amélioration (obligatoire)",
|
||||
taskSection: 'Arbre des tâches', newTask: '+ Nouvelle tâche', titleLabel: 'Titre', titlePlaceholder: 'Que faut-il faire',
|
||||
complexity: 'Complexité', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(racine)', priority: 'Priorité',
|
||||
@@ -249,7 +249,7 @@ window.MAESTRO_I18N = {
|
||||
gatePassed: ' approuvée',
|
||||
status: { init: 'Nouvelle', analyzing: 'Analyse', plan_review: 'Revue du plan', decomposed: 'Décomposée', speccing: 'Rédaction spec', spec_review: 'Revue de spec', ready: 'Prête', blocked: 'Bloquée', queued: 'En file', executing: 'En exécution', exec_review: 'Revue & fusion', failed: 'Échouée', needs_attention: 'À traiter', done: 'Terminée', paused: 'En pause', cancelled: 'Annulée' },
|
||||
gates: { plan: 'REVUE DU PLAN', spec: 'REVUE DE SPEC', exec: 'REVUE DU RÉSULTAT' },
|
||||
gateSubtasks: 'Sous-tâches', noDeps: 'sans deps', dependsOn: 'deps',
|
||||
gateSubtasks: 'Sous-tâches', frozenPending: 'en attente', scopeFiles: 'fichiers', planTranscript: 'Trace de décomposition', noDeps: 'sans deps', dependsOn: 'deps',
|
||||
lblCodeReview: 'Revue', lblSecurity: 'Sécurité', lblChanges: 'Modifs', vApprove: 'Approuver', vReject: 'Rejeter', vNone: 'Sans verdict',
|
||||
eventTypes: { 'task.created': 'Tâche créée', 'task.updated': 'Tâche mise à jour', 'status.changed': "Changement d'état", 'approval.requested': 'Approbation demandée', 'approval.granted': 'Approbation accordée', 'approval.rejected': 'Approbation rejetée', 'run.started': 'Run démarré', 'run.finished': 'Run terminé', 'project.synced': 'Todo synchronisé' },
|
||||
},
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
<!-- Markdown 渲染(window.MaestroRichDoc 依赖)-->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/styles/github-dark.min.css" integrity="sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js" integrity="sha384-/TQbtLCAerC3jgaim+N78RZSDYV7ryeoBCVqTuzRrFec2akfBkHS7ACQ3PQhvMVi" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11.9.0/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous"></script>
|
||||
<script src="../../_ds_bundle.js"></script>
|
||||
<script src="data.js"></script>
|
||||
<script src="i18n.js"></script>
|
||||
@@ -37,6 +42,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="RichDoc.jsx"></script>
|
||||
<script type="text/babel" src="Transcript.jsx"></script>
|
||||
<script type="text/babel" src="Sidebar.jsx"></script>
|
||||
<script type="text/babel" src="EventPanel.jsx"></script>
|
||||
<script type="text/babel" src="Topbar.jsx"></script>
|
||||
|
||||
Reference in New Issue
Block a user