// 任务树:筛选栏(搜索+复杂度+状态分组)+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情;新建任务表单
// 状态全序(下拉选项顺序:待办 → 进行 → 容器 → 审批 → 异常 → 挂起 → 终态)
const KIT_STATUS_ORDER = [
'init', 'ready', 'blocked',
'analyzing', 'speccing', 'queued', 'executing', 'decomposed',
'plan_review', 'spec_review', 'exec_review',
'failed', 'needs_attention',
'paused', 'done', 'cancelled',
];
const KIT_CPLX_OPTS = [{ value: 'hard', label: 'HARD' }, { value: 'medium', label: 'MED' }, { value: 'easy', label: 'EASY' }];
const KIT_PRIO_OPTS = [{ value: 'P0', label: 'P0' }, { value: 'P1', label: 'P1' }, { value: 'P2', label: 'P2' }];
// 主显示区各区块标题通用折叠按钮(▾ 展开 / ▸ 折叠);暴露到 window 供 Agent/审批闸/归档复用
function KitFoldBtn({ folded, onClick, t }) {
return (
);
}
window.MaestroKitFoldBtn = KitFoldBtn;
// 区块图标集(24 网格 / stroke currentColor,继承标题 accent 色)
function KitIcon({ name }) {
const p = { width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none', display: 'block' } };
if (name === 'agent') return ;
if (name === 'gate') return ;
if (name === 'tasks') return ;
if (name === 'archive') return ;
return null;
}
// 统一区块标题:图标 + 名称 + 数量徽标 + 折叠按钮(四块主显示区共用,视觉一致)
function KitSectionHead({ icon, title, count, accent = 'var(--green)', folded, onToggleFold, t, sticky }) {
return (
{title}
{count != null ? (
{count}
) : null}
);
}
window.MaestroKitSectionHead = KitSectionHead;
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}
);
}
// 多选下拉:触发器显示「标签 ·N」,菜单内勾选切换(不随单击关闭);点外 / Esc 关闭
function KitMultiSelect({ label, options, selected, onToggle, onClear, accent = 'var(--green)' }) {
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); };
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
}, [open]);
const n = selected.size;
// 已选项统一排到菜单最前(打开时按「选中优先」重排,保持各自原相对顺序)
const ordered = open
? [...options.filter((o) => selected.has(o.value)), ...options.filter((o) => !selected.has(o.value))]
: options;
return (
{open ? (
{n ? (
) : null}
{ordered.map((o) => {
const on = selected.has(o.value);
return (
);
})}
) : null}
);
}
function FilterBar({ t, kw, setKw, sets, toggle, clearOne, matchCount, filtering, onClear }) {
const { Button } = window.MaestroDesignSystem_a6a290;
const statusOpts = KIT_STATUS_ORDER.map((s) => ({ value: s, label: t.status[s] }));
const assigneeOpts = [{ value: 'agent', label: t.assignees.agent }, { value: 'human', label: t.assignees.human }];
return (
setKw(e.target.value)} placeholder={t.searchPh} autoComplete="off" style={{
fontFamily: 'var(--mono)', fontSize: 12, width: 180, padding: '5px 9px',
background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)',
borderRadius: 'var(--radius-sm, 4px)', outline: 'none',
}} />
toggle('status', v)} onClear={() => clearOne('status')} />
toggle('cplx', v)} onClear={() => clearOne('cplx')} accent="var(--amber)" />
toggle('prio', v)} onClear={() => clearOne('prio')} accent="var(--red)" />
toggle('assignee', v)} onClear={() => clearOne('assignee')} accent="var(--violet)" />
{filtering ? {t.matchCount.replace('{n}', matchCount)} : null}
{filtering ? : null}
);
}
function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleOpen, t, byId, cplxOf, onChangeCplx, flashId, onJump, visibleSet, onTakeover, taskOps }) {
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}
{task.frozen
?
: }
{expanded ?
: null}
{kids.length && open ? (
{kids.map((k) => (
))}
) : null}
);
}
function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
const label = task.doc ? t.specLabel : task.ops ? t.opsLabel : null;
const text = task.doc || task.ops;
const attachments = (task._raw && task._raw.attachments) || [];
const ops = taskOps || {};
const st = task.status;
const canCancel = !['done', 'cancelled'].includes(st);
const canRequeue = ['failed', 'needs_attention', 'blocked', 'cancelled'].includes(st);
const [editing, setEditing] = React.useState(false);
const [eTitle, setETitle] = React.useState(task.title);
const [ePrio, setEPrio] = React.useState(String((task.prio || 'P1').replace('P', '')));
const [eDeps, setEDeps] = React.useState(task.deps || []);
const btn = (color) => ({ fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer', background: 'transparent', color: `var(--${color})`, border: `1px solid var(--${color}-dim)`, borderRadius: 'var(--radius-sm,4px)', padding: '3px 10px' });
const candidates = [...byId.values()].filter((x) => x.id !== task.id);
const toggleDep = (id) => setEDeps((d) => (d.includes(id) ? d.filter((x) => x !== id) : [...d, id]));
const saveEdit = () => {
if (ops.patch) ops.patch(task.id, { title: eTitle.trim(), priority: Number(ePrio), deps: eDeps });
setEditing(false);
};
return (
{/* 操作工具条:编辑 / 接管 / 重投 / 取消 / 删除 */}
e.stopPropagation()}>
{ops.patch ? : null}
{onTakeover ? : null}
{ops.requeue && canRequeue ? : null}
{ops.cancel && canCancel ? : null}
{ops.del ? : null}
{attachments.length ? 📎 {attachments.map((a) => a.name).join(' · ')} : null}
{/* 编辑表单 */}
{editing ? (
e.stopPropagation()}>
{candidates.length ? (
依赖(点选其他任务)
{candidates.map((c) => {
const on = eDeps.includes(c.id);
return (
);
})}
) : null}
) : null}
{label ? (
) : (
{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}
);
}
// 新建任务面板的附件区样式(dropzone 高亮 / 缩略图 chip / 删除角标),一次性注入避免污染全局
function ensureNewTaskCss() {
if (document.getElementById('maestro-kit-newtask-css')) return;
const s = document.createElement('style');
s.id = 'maestro-kit-newtask-css';
s.textContent = `
.m-nt-drop { display: flex; flex-direction: column; gap: 8px; padding: 10px; border: 1px dashed var(--line); border-radius: var(--radius-sm, 4px); background: var(--bg-deep); transition: border-color .12s, background .12s; }
.m-nt-drop.is-over { border-color: var(--green-dim); background: var(--panel-2); }
.m-nt-hint { font-size: 10.5px; color: var(--faint); letter-spacing: .04em; }
.m-nt-hint.is-over { color: var(--green); }
.m-nt-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.m-nt-chip { position: relative; width: 92px; display: flex; flex-direction: column; gap: 3px; }
.m-nt-thumb { width: 92px; height: 64px; border: 1px solid var(--line); border-radius: var(--radius-sm, 4px); background: var(--panel-2); overflow: hidden; display: flex; align-items: center; justify-content: center; }
.m-nt-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.m-nt-ext { font-family: var(--mono); font-size: 12px; color: var(--cyan); text-transform: uppercase; letter-spacing: .06em; }
.m-nt-name { font-size: 10px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.m-nt-size { font-size: 9.5px; color: var(--faint); font-family: var(--mono); }
.m-nt-del { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; padding: 0; line-height: 1; border: 1px solid var(--line); border-radius: 50%; background: var(--panel); color: var(--muted); cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: color .12s, border-color .12s, background .12s; }
.m-nt-del:hover { color: var(--red); border-color: var(--red-dim); background: var(--panel-2); }
`;
document.head.appendChild(s);
}
// 三来源(选择/粘贴/拖拽)统一去重键:name+size+lastModified
function ntFileKey(f) { return (f.name || '') + ' ' + f.size + ' ' + f.lastModified; }
// 由 MIME 推断扩展名(剪贴板图片常无文件名)
function ntExtFromType(type) {
if (!type) return '';
const sub = String(type).split('/')[1] || '';
const m = { jpeg: 'jpg', svg: 'svg', 'svg+xml': 'svg' };
return sub ? '.' + (m[sub] || sub) : '';
}
// 为无名文件(粘贴的截图)补一个稳定文件名
function ntNamed(f, i) {
if (f.name) return f;
const name = 'pasted-' + Date.now() + (i ? '-' + i : '') + ntExtFromType(f.type);
try { return new File([f], name, { type: f.type, lastModified: f.lastModified }); } catch { return f; }
}
// chip 角标显示的扩展名文字
function ntExtLabel(f) {
const fromName = f.name && f.name.includes('.') ? f.name.split('.').pop() : '';
return (fromName || ntExtFromType(f.type).replace(/^\./, '') || 'file').toUpperCase();
}
function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
ensureNewTaskCss();
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 [dragOver, setDragOver] = React.useState(false);
// 图片缩略图 objectURL 缓存(按去重键),卸载/移除时显式 revoke 防泄漏
const urlMapRef = React.useRef(new Map());
const thumbUrl = (f) => {
if (!f.type || !f.type.startsWith('image/')) return null;
const key = ntFileKey(f);
const map = urlMapRef.current;
if (!map.has(key)) map.set(key, URL.createObjectURL(f));
return map.get(key);
};
// 组件卸载时释放全部 objectURL
React.useEffect(() => () => {
urlMapRef.current.forEach((u) => URL.revokeObjectURL(u));
urlMapRef.current.clear();
}, []);
// 合并新文件并按复合键去重(剪贴板无名图先补名)
const addFiles = (incoming) => {
if (!incoming || !incoming.length) return;
const named = Array.from(incoming).map((f, i) => ntNamed(f, i));
setFiles((prev) => {
const seen = new Set(prev.map(ntFileKey));
const merged = prev.slice();
named.forEach((f) => { const k = ntFileKey(f); if (!seen.has(k)) { seen.add(k); merged.push(f); } });
return merged;
});
};
const removeFile = (key) => {
setFiles((prev) => prev.filter((f) => ntFileKey(f) !== key));
const map = urlMapRef.current;
if (map.has(key)) { URL.revokeObjectURL(map.get(key)); map.delete(key); }
};
const onPaste = (e) => {
const items = (e.clipboardData && e.clipboardData.items) || [];
const imgs = [];
for (let i = 0; i < items.length; i++) {
const it = items[i];
if (it.kind === 'file' && it.type && it.type.startsWith('image/')) {
const f = it.getAsFile();
if (f) imgs.push(f);
}
}
if (imgs.length) { e.preventDefault(); addFiles(imgs); }
};
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 (
);
}
function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
const [expandedId, setExpandedId] = React.useState(null);
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
const [folded, setFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-tasks') === '1');
const toggleFold = () => setFolded((f) => { localStorage.setItem('maestro-kit-fold-tasks', f ? '0' : '1'); return !f; });
const [kw, setKw] = React.useState('');
const [statusSet, setStatusSet] = React.useState(() => new Set());
const [cplxSet, setCplxSet] = React.useState(() => new Set());
const [prioSet, setPrioSet] = React.useState(() => new Set());
const [assigneeSet, setAssigneeSet] = 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 })); // 乐观显示
if (taskOps && taskOps.patch) taskOps.patch(id, { complexity: v }); // 落库
};
const toggleOpen = (id) => setOpenIds((prev) => {
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
});
// 四维多选筛选:status / cplx / prio / assignee
const setters = { status: setStatusSet, cplx: setCplxSet, prio: setPrioSet, assignee: setAssigneeSet };
const sets = { status: statusSet, cplx: cplxSet, prio: prioSet, assignee: assigneeSet };
const toggle = (key, v) => setters[key]((prev) => {
const next = new Set(prev); next.has(v) ? next.delete(v) : next.add(v); return next;
});
const clearOne = (key) => setters[key](new Set());
const filtering = kw.trim() !== '' || statusSet.size > 0 || cplxSet.size > 0 || prioSet.size > 0 || assigneeSet.size > 0;
const clearFilters = () => { setKw(''); setStatusSet(new Set()); setCplxSet(new Set()); setPrioSet(new Set()); setAssigneeSet(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())) &&
(statusSet.size === 0 || statusSet.has(tk.status)) &&
(cplxSet.size === 0 || cplxSet.has(cplxOverride[tk.id] || tk.cplx)) &&
(prioSet.size === 0 || prioSet.has(tk.prio)) &&
(assigneeSet.size === 0 || assigneeSet.has((tk._raw && tk._raw.assignee) || ''));
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, statusSet, cplxSet, prioSet, assigneeSet, 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 (
{!folded ? (
{roots.map((tk) => (
))}
) : null}
);
}
Object.assign(window, { MaestroKitTaskSection: TaskSection, MaestroKitNewTaskPanel: NewTaskPanel });