// 任务树:筛选栏(搜索+复杂度+状态分组)+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情;新建任务表单 const KIT_FILTER_GROUPS = [ ['todo', ['init', 'ready', 'blocked']], ['doing', ['analyzing', 'speccing', 'queued', 'executing', 'decomposed']], ['gate', ['plan_review', 'spec_review', 'exec_review']], ['bad', ['failed', 'needs_attention']], ['hold', ['paused', 'cancelled']], ['done', ['done']], ]; function kitFlatten(tasks, map = new Map()) { for (const tk of tasks) { map.set(tk.id, tk); if (tk.children) kitFlatten(tk.children, map); } return map; } // 复杂度徽章 + 点击换档下拉 function CplxPicker({ task, cplx, onChange }) { const { ComplexityBadge } = window.MaestroDesignSystem_a6a290; const [open, setOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); return ( e.stopPropagation()}> {open ? ( {['hard', 'medium', 'easy'].map((v) => ( ))} ) : null} ); } function FilterBar({ t, kw, setKw, cplxSet, toggleCplx, statusSet, toggleGroup, matchCount, filtering, onClear }) { const { Button } = window.MaestroDesignSystem_a6a290; const chip = (on, color, dim, label, onClick) => ( ); return (
setKw(e.target.value)} placeholder={t.searchPh} autoComplete="off" style={{ fontFamily: 'var(--mono)', fontSize: 12, width: 190, padding: '4px 9px', background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)', borderRadius: 'var(--radius-sm, 4px)', outline: 'none', }} /> {chip(cplxSet.has('hard'), 'var(--red)', 'var(--red-dim)', 'HARD', () => toggleCplx('hard'))} {chip(cplxSet.has('medium'), 'var(--amber)', 'var(--amber-dim)', 'MED', () => toggleCplx('medium'))} {chip(cplxSet.has('easy'), 'var(--green)', 'var(--green-dim)', 'EASY', () => toggleCplx('easy'))} {filtering ? {t.matchCount.replace('{n}', matchCount)} : null} {filtering ? : null}
{KIT_FILTER_GROUPS.map(([g, sts]) => { const on = sts.every((s) => statusSet.has(s)); const part = !on && sts.some((s) => statusSet.has(s)); return ( ); })}
); } function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleOpen, t, byId, cplxOf, onChangeCplx, flashId, onJump, visibleSet }) { const { StatusChip } = window.MaestroDesignSystem_a6a290; const kids = (task.children || []).filter((k) => !visibleSet || visibleSet.has(k.id)); const open = openIds.has(task.id) || !!visibleSet; // 筛选时自动展开可见节点 const expanded = expandedId === task.id; const isGate = ['plan_review', 'spec_review', 'exec_review'].includes(task.status); const flashing = flashId === task.id; const [hover, setHover] = React.useState(false); return (
0 ? { borderLeft: '1px solid var(--line-soft)' } : null}>
setExpandedId(expanded ? null : task.id)} data-task-id={task.id} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px 8px 6px', borderBottom: '1px solid var(--line-soft)', cursor: 'pointer', transition: 'background .1s', background: expanded ? 'var(--panel-2)' : isGate ? 'rgba(184,142,245,.05)' : hover ? 'var(--panel)' : 'transparent', opacity: ctx ? .55 : 1, animation: flashing ? 'maestro-locate 1.8s ease-out' : 'none', }}> { e.stopPropagation(); if (kids.length) toggleOpen(task.id); }} style={{ width: 16, flex: 'none', textAlign: 'center', fontSize: 10, userSelect: 'none', color: kids.length ? (open ? 'var(--green)' : 'var(--muted)') : 'var(--faint)', transform: kids.length && open ? 'rotate(90deg)' : 'none', transition: 'transform .12s', }}> {kids.length ? '▶' : '·'} {task.title}{task.id} {task.deps ? ( {t.depsWait} {task.deps.length} ) : null} {task.prio}
{expanded ? : null} {kids.length && open ? (
{kids.map((k) => ( ))}
) : null}
); } function TaskDetail({ task, t, byId, onJump }) { const label = task.doc ? t.specLabel : task.ops ? t.opsLabel : null; const text = task.doc || task.ops; return (
{label ? (
{label}
{text}
) : (
{t.pendingDoc}
)} {task.deps && task.deps.length ? (
{t.depsLabel}
{task.deps.map((d) => { const dep = byId.get(d); const ok = dep && dep.status === 'done'; return (
{ e.stopPropagation(); onJump(d); }} title="→" style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, padding: '4px 10px', cursor: 'pointer', background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 4, borderLeft: '2px solid ' + (ok ? 'var(--green-dim)' : 'var(--amber-dim)'), transition: 'border-color .12s, background .12s', }} onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-2)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--bg-deep)'; }}> {ok ? '✓' : '◌'} {dep ? dep.title : d} {ok ? t.depDone : t.depWait}
); })}
) : null}
); } function NewTaskPanel({ tasks, onCancel, onCreate, t }) { const { Button, Input, Select, ComplexitySeg } = window.MaestroDesignSystem_a6a290; const [title, setTitle] = React.useState(''); const [complexity, setComplexity] = React.useState('auto'); const [priority, setPriority] = React.useState('1'); const [parentId, setParentId] = React.useState(''); const [files, setFiles] = React.useState([]); const submit = (e) => { e.preventDefault(); if (!title.trim()) return; onCreate({ title: title.trim(), complexity, priority: Number(priority), parentId: parentId || null, files }); }; // 扁平化任务树供父任务下拉(含子任务) const flat = []; const walk = (list, prefix) => (list || []).forEach((tk) => { flat.push({ value: tk.id, label: prefix + tk.title }); walk(tk.children, prefix + '— '); }); walk(tasks, ''); return (
{files.length ? (
{files.map((f) => f.name).join(' · ')}
) : null}
); } function TaskSection({ tasks, onToast, onCreate, t }) { const { Button, SectionHead } = window.MaestroDesignSystem_a6a290; const [expandedId, setExpandedId] = React.useState(null); const [openIds, setOpenIds] = React.useState(() => new Set(['t1'])); const [showNew, setShowNew] = React.useState(false); const [kw, setKw] = React.useState(''); const [cplxSet, setCplxSet] = React.useState(() => new Set()); const [statusSet, setStatusSet] = React.useState(() => new Set()); const [cplxOverride, setCplxOverride] = React.useState({}); const [flashId, setFlashId] = React.useState(null); const byId = React.useMemo(() => kitFlatten(tasks), [tasks]); const cplxOf = (task) => cplxOverride[task.id] || task.cplx; const onChangeCplx = (id, v) => setCplxOverride((m) => ({ ...m, [id]: v })); const toggleOpen = (id) => setOpenIds((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); const toggleCplx = (v) => setCplxSet((prev) => { const next = new Set(prev); next.has(v) ? next.delete(v) : next.add(v); return next; }); const toggleGroup = (sts) => setStatusSet((prev) => { const next = new Set(prev); const allOn = sts.every((s) => next.has(s)); for (const s of sts) allOn ? next.delete(s) : next.add(s); return next; }); const filtering = kw.trim() !== '' || cplxSet.size > 0 || statusSet.size > 0; const clearFilters = () => { setKw(''); setCplxSet(new Set()); setStatusSet(new Set()); }; // 筛选:自身命中 → 显示;祖先链作为上下文淡显;命中节点的父级自动展开 const { visibleSet, matchCount } = React.useMemo(() => { if (!filtering) return { visibleSet: null, matchCount: 0 }; const matches = (tk) => (kw.trim() === '' || tk.title.toLowerCase().includes(kw.trim().toLowerCase())) && (cplxSet.size === 0 || cplxSet.has(cplxOverride[tk.id] || tk.cplx)) && (statusSet.size === 0 || statusSet.has(tk.status)); const vis = new Set(); const ctx = new Set(); let count = 0; const walk = (tk) => { let childHit = false; for (const k of tk.children || []) if (walk(k)) childHit = true; const hit = matches(tk); if (hit) count++; if (hit || childHit) { vis.add(tk.id); if (!hit) ctx.add(tk.id); return true; } return false; }; for (const tk of tasks) walk(tk); const set = new Set(vis); set.ctx = ctx; return { visibleSet: set, matchCount: count }; }, [filtering, kw, cplxSet, statusSet, tasks, cplxOverride]); // 依赖跳转:展开所有祖先 + flash 定位 const onJump = (id) => { setOpenIds((prev) => { const next = new Set(prev); const openAncestors = (list, chain) => { for (const tk of list) { if (tk.id === id) { for (const c of chain) next.add(c); return true; } if (tk.children && openAncestors(tk.children, [...chain, tk.id])) return true; } return false; }; openAncestors(tasks, []); return next; }); setFlashId(null); requestAnimationFrame(() => setFlashId(id)); setTimeout(() => setFlashId((f) => (f === id ? null : f)), 1900); }; const roots = visibleSet ? tasks.filter((tk) => visibleSet.has(tk.id)) : tasks; return (
setShowNew(!showNew)}>{t.newTask}} /> {showNew ? setShowNew(false)} onCreate={(payload) => { setShowNew(false); onCreate ? onCreate(payload) : onToast('ok', t.toastCreated); }} /> : null}
{roots.map((tk) => ( ))}
); } Object.assign(window, { MaestroKitTaskSection: TaskSection });