// 任务树：筛选栏（搜索+复杂度+状态分组）+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情；新建任务表单
// 状态全序（下拉选项顺序：待办 → 进行 → 容器 → 审批 → 异常 → 挂起 → 终态）
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 (
    <button type="button" onClick={onClick} title={folded ? (t.gateExpand || '展开') : (t.gateCollapse || '折叠')} style={{
      background: 'transparent', border: '1px solid var(--line)', color: 'var(--muted)', cursor: 'pointer',
      fontSize: 10, fontFamily: 'var(--mono)', lineHeight: 1, padding: '3px 8px', borderRadius: 'var(--radius-sm,4px)',
    }}
      onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--green)'; e.currentTarget.style.borderColor = 'var(--green-dim)'; }}
      onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
      {folded ? '▸' : '▾'}
    </button>
  );
}
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 <svg {...p}><polyline points="3 12 7 12 10 5 14 19 17 12 21 12" /></svg>;
  if (name === 'gate') return <svg {...p}><path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z" /><path d="M9 12l2 2 4-4" /></svg>;
  if (name === 'tasks') return <svg {...p}><path d="M9 6h11M9 12h11M9 18h11" /><path d="M4.5 6h.01M4.5 12h.01M4.5 18h.01" /></svg>;
  if (name === 'archive') return <svg {...p}><rect x="3" y="4" width="18" height="4" rx="1" /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><path d="M10 12h4" /></svg>;
  return null;
}

// 统一区块标题：图标 + 名称 + 数量徽标 + 折叠按钮（四块主显示区共用，视觉一致）
function KitSectionHead({ icon, title, count, accent = 'var(--green)', folded, onToggleFold, t, sticky }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 9,
      fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.2em',
      color: 'var(--muted)', textTransform: 'uppercase', padding: '16px 0 10px',
      ...(sticky ? { position: 'sticky', top: 0, zIndex: 20, background: 'var(--bg)', boxShadow: '0 1px 0 var(--line-soft), 0 6px 10px -8px rgba(0,0,0,.6)' } : {}),
    }}>
      <span style={{ color: accent, flex: 'none', display: 'inline-flex', alignItems: 'center' }}><KitIcon name={icon} /></span>
      <span style={{ whiteSpace: 'nowrap' }}>{title}</span>
      {count != null ? (
        <span style={{
          flex: 'none', fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 700, letterSpacing: '.02em',
          color: accent, background: 'var(--panel-2)', borderRadius: 10, padding: '1px 7px', lineHeight: '15px',
          minWidth: 10, textAlign: 'center',
        }}>{count}</span>
      ) : null}
      <span style={{ marginLeft: 'auto', flex: 'none' }}><KitFoldBtn folded={folded} onClick={onToggleFold} t={t} /></span>
    </div>
  );
}
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 (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }} onClick={(e) => e.stopPropagation()}>
      <button onClick={() => setOpen(!open)} title="点击调整复杂度" style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex' }}>
        <ComplexityBadge complexity={cplx} />
      </button>
      {open ? (
        <span style={{
          position: 'absolute', top: 'calc(100% + 5px)', right: 0, zIndex: 60,
          display: 'flex', gap: 5, background: 'var(--bg-deep)', border: '1px solid var(--line)',
          borderRadius: 'var(--radius-md, 6px)', padding: 6, boxShadow: '0 10px 30px rgba(0,0,0,.65)',
          animation: 'maestro-rise .12s ease both',
        }}>
          {['hard', 'medium', 'easy'].map((v) => (
            <button key={v} onClick={() => { onChange(task.id, v); setOpen(false); }} style={{
              background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'inline-flex',
              outline: v === cplx ? '1px solid currentColor' : 'none', outlineOffset: 1,
              filter: 'none',
            }}
              onMouseEnter={(e) => { e.currentTarget.style.filter = 'brightness(1.35)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.filter = 'none'; }}>
              <ComplexityBadge complexity={v} />
            </button>
          ))}
        </span>
      ) : null}
    </span>
  );
}

// 多选下拉：触发器显示「标签 ·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 (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>
      <button type="button" onClick={() => setOpen(!open)} style={{
        fontFamily: 'var(--mono)', fontSize: 11.5, letterSpacing: '.03em',
        background: n ? 'rgba(95,221,125,.08)' : 'var(--bg-deep)',
        color: n ? accent : 'var(--muted)',
        border: '1px solid ' + (n ? 'var(--green-dim)' : 'var(--line)'),
        borderRadius: 'var(--radius-sm, 4px)', padding: '5px 9px', cursor: 'pointer',
        display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', transition: 'all .1s',
      }}>
        <span>{label}</span>
        {n ? <span style={{
          fontSize: 9.5, fontWeight: 700, background: accent, color: 'var(--bg-deep)',
          borderRadius: 8, padding: '0 5px', lineHeight: '14px', minWidth: 8, textAlign: 'center',
        }}>{n}</span> : null}
        <span style={{ fontSize: 8, color: 'var(--faint)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}>▼</span>
      </button>
      {open ? (
        <span style={{
          position: 'absolute', top: 'calc(100% + 5px)', left: 0, zIndex: 80, minWidth: 152,
          display: 'grid', gap: 1, background: 'var(--bg-deep)', border: '1px solid var(--line)',
          borderRadius: 'var(--radius-md, 6px)', padding: 4, boxShadow: '0 10px 30px rgba(0,0,0,.65)',
          animation: 'maestro-rise .12s ease both', maxHeight: 320, overflowY: 'auto',
        }}>
          {n ? (
            <button type="button" onClick={onClear} style={{
              fontFamily: 'var(--mono)', fontSize: 11, textAlign: 'left', background: 'transparent',
              color: 'var(--faint)', border: 'none', borderRadius: 4, padding: '5px 8px', cursor: 'pointer',
              marginBottom: 2, borderBottom: '1px solid var(--line)',
            }}>✕ 清空</button>
          ) : null}
          {ordered.map((o) => {
            const on = selected.has(o.value);
            return (
              <button type="button" key={o.value} onClick={() => onToggle(o.value)} style={{
                fontFamily: 'var(--mono)', fontSize: 12, textAlign: 'left',
                background: on ? 'var(--panel-2)' : 'transparent',
                color: on ? accent : 'var(--ink)', fontWeight: on ? 700 : 400,
                border: 'none', borderRadius: 4, padding: '6px 9px 6px 7px', cursor: 'pointer',
                display: 'flex', alignItems: 'center', gap: 7, whiteSpace: 'nowrap',
              }}
                onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = 'var(--panel)'; }}
                onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
                <span style={{ flex: 'none', width: 10, color: accent, fontSize: 10 }}>{on ? '✓' : ''}</span>{o.label}
              </button>
            );
          })}
        </span>
      ) : null}
    </span>
  );
}

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 (
    <div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px', marginBottom: 12, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
      <input value={kw} onChange={(e) => 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',
      }} />
      <KitMultiSelect label={t.fStatus} options={statusOpts} selected={sets.status} onToggle={(v) => toggle('status', v)} onClear={() => clearOne('status')} />
      <KitMultiSelect label={t.complexity} options={KIT_CPLX_OPTS} selected={sets.cplx} onToggle={(v) => toggle('cplx', v)} onClear={() => clearOne('cplx')} accent="var(--amber)" />
      <KitMultiSelect label={t.priority} options={KIT_PRIO_OPTS} selected={sets.prio} onToggle={(v) => toggle('prio', v)} onClear={() => clearOne('prio')} accent="var(--red)" />
      <KitMultiSelect label={t.fAssignee} options={assigneeOpts} selected={sets.assignee} onToggle={(v) => toggle('assignee', v)} onClear={() => clearOne('assignee')} accent="var(--violet)" />
      <span style={{ flex: 1 }}></span>
      {filtering ? <span style={{ fontSize: 11, color: 'var(--amber)', letterSpacing: '.08em' }}>{t.matchCount.replace('{n}', matchCount)}</span> : null}
      {filtering ? <Button variant="ghost" size="xs" onClick={onClear}>{t.clearFilter}</Button> : null}
    </div>
  );
}

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 (
    <div style={depth > 0 ? { borderLeft: '1px solid var(--line-soft)' } : null}>
      <div onClick={() => 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',
        }}>
        <span onClick={(e) => { 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 ? '▶' : '·'}
        </span>
        <span style={{ fontWeight: 500, color: ctx ? 'var(--muted)' : 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {task.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{task.id}</span>
        </span>
        <span style={{ flex: 1, minWidth: 8 }}></span>
        {task.deps ? (
          <span style={{ flex: 'none', fontSize: 10.5, letterSpacing: '.06em', color: 'var(--amber)', border: '1px dashed var(--amber-dim)', borderRadius: 3, padding: '1px 7px', lineHeight: 1.5, whiteSpace: 'nowrap' }}>
            {t.depsWait} {task.deps.length}
          </span>
        ) : 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} />
        {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 ? (
        <div style={{ marginLeft: 22 }}>
          {kids.map((k) => (
            <TaskRow key={k.id} task={k} depth={depth + 1} ctx={visibleSet ? visibleSet.ctx.has(k.id) : false}
              expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
              byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} />
          ))}
        </div>
      ) : null}
    </div>
  );
}

function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) {
  ensureNewTaskCss();
  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 (
    <div style={{ background: 'var(--panel)', borderBottom: '1px solid var(--line)', borderLeft: '2px solid var(--green-dim)', padding: '14px 16px', animation: 'maestro-rise .2s ease both', display: 'grid', gap: 14 }}>
      {/* 操作工具条：编辑 / 接管 / 重投 / 取消 / 删除 */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }} onClick={(e) => e.stopPropagation()}>
        {ops.patch ? <button onClick={() => setEditing(!editing)} style={btn(editing ? 'green' : 'muted')}>✎ 编辑</button> : null}
        {onTakeover ? <button onClick={() => onTakeover(task)} style={btn('cyan')}>⌨ 人工接管</button> : null}
        {ops.requeue && canRequeue ? <button onClick={() => ops.requeue(task.id)} style={btn('green')}>↻ 重投</button> : null}
        {ops.cancel && canCancel ? <button onClick={() => ops.cancel(task.id)} style={btn('amber')}>⊘ 取消</button> : null}
        {ops.del ? <button onClick={() => { if (confirm('删除任务「' + task.title + '」?子任务一并删除,不可恢复。')) ops.del(task.id); }} style={btn('red')}>🗑 删除</button> : null}
      </div>
      {/* 附件画廊：图片缩略图（点击新标签预览）+ 下载 + 删除（确认后调用 taskOps.delAttachment） */}
      {attachments.length ? (
        <div onClick={(e) => e.stopPropagation()}>
          <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>{t.attSection} · {attachments.length}</div>
          <div className="m-nt-grid">
            {attachments.map((a) => {
              const dn = attDiskName(a);
              const isImg = (a.type || '').startsWith('image/');
              const url = attUrl(task.id, dn);
              return (
                <div key={dn || a.name} className="m-nt-chip">
                  <a className="m-att-thumb-link" href={url} target="_blank" rel="noopener noreferrer" title={a.name}>
                    <span className="m-nt-thumb">
                      {isImg ? <img src={url} alt={a.name} /> : <span className="m-nt-ext">{(a.name.includes('.') ? a.name.split('.').pop() : (a.type || 'file').split('/').pop() || 'file').toUpperCase()}</span>}
                    </span>
                  </a>
                  <span className="m-nt-name" title={a.name}>{a.name}</span>
                  <span className="m-att-act">
                    <a className="m-att-btn" href={attUrl(task.id, dn, true)} download={a.name}>↓ {t.attDownload}</a>
                    {ops.delAttachment ? (
                      <button type="button" className="m-att-btn is-del"
                        onClick={() => { if (confirm(t.attDeleteConfirm.replace('{name}', a.name))) ops.delAttachment(task.id, dn); }}>✕</button>
                    ) : null}
                  </span>
                </div>
              );
            })}
          </div>
        </div>
      ) : null}
      {/* 编辑表单 */}
      {editing ? (
        <div style={{ display: 'grid', gap: 12, padding: 12, border: '1px solid var(--line)', borderRadius: 6, background: 'var(--bg-deep)' }} onClick={(e) => e.stopPropagation()}>
          <Input label="标题" value={eTitle} onChange={setETitle} style={{ width: '100%' }} />
          <div style={{ display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap' }}>
            <Select label={t.priority} value={ePrio} onChange={setEPrio} options={[{ value: '0', label: t.p0 }, { value: '1', label: t.p1 }, { value: '2', label: t.p2 }]} />
          </div>
          {candidates.length ? (
            <div>
              <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.12em', marginBottom: 6 }}>依赖（点选其他任务）</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, maxHeight: 120, overflowY: 'auto' }}>
                {candidates.map((c) => {
                  const on = eDeps.includes(c.id);
                  return (
                    <button key={c.id} onClick={() => toggleDep(c.id)}
                      style={{ fontFamily: 'var(--mono)', fontSize: 11, cursor: 'pointer', padding: '2px 8px', borderRadius: 3, border: '1px solid ' + (on ? 'var(--green-dim)' : 'var(--line)'), background: on ? 'var(--green-dim)' : 'transparent', color: on ? '#fff' : 'var(--muted)', maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {on ? '✓ ' : ''}{c.title}
                    </button>
                  );
                })}
              </div>
            </div>
          ) : null}
          <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
            <Button onClick={() => setEditing(false)}>{t.cancel}</Button>
            <Button variant="solid" disabled={!eTitle.trim()} onClick={saveEdit}>{t.save}</Button>
          </div>
        </div>
      ) : null}
      {label ? (
        <div>
          <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{label}</div>
          <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 }}>
          {t.pendingDoc}
        </div>
      )}
      {task.deps && task.deps.length ? (
        <div>
          <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{t.depsLabel}</div>
          <div style={{ display: 'grid', gap: 4 }}>
            {task.deps.map((d) => {
              const dep = byId.get(d);
              const ok = dep && dep.status === 'done';
              return (
                <div key={d} onClick={(e) => { 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)'; }}>
                  <span style={{ color: ok ? 'var(--green)' : 'var(--amber)', flex: 'none' }}>{ok ? '✓' : '◌'}</span>
                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{dep ? dep.title : d}</span>
                  <span style={{ flex: 'none', fontSize: 10.5, color: ok ? 'var(--green)' : 'var(--amber)' }}>{ok ? t.depDone : t.depWait}</span>
                  <span style={{ marginLeft: 'auto', color: 'var(--faint)', fontSize: 12 }}>→</span>
                </div>
              );
            })}
          </div>
        </div>
      ) : null}
    </div>
  );
}

// 新建任务面板的附件区样式（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); }
.m-att-thumb-link { display: block; width: 92px; height: 64px; }
.m-att-act { display: flex; gap: 6px; align-items: center; }
.m-att-btn { font-family: var(--mono); font-size: 10px; line-height: 1; padding: 3px 6px; border: 1px solid var(--line); border-radius: var(--radius-sm, 4px); background: var(--panel); color: var(--muted); cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; transition: color .12s, border-color .12s, background .12s; }
.m-att-btn:hover { color: var(--green); border-color: var(--green-dim); background: var(--panel-2); }
.m-att-btn.is-del:hover { color: var(--red); border-color: var(--red-dim); }
`;
  document.head.appendChild(s);
}

// 已建任务附件：磁盘文件名（后端 :name 标识符 = basename(att.path)，非展示名 att.name）
function attDiskName(a) { return (a.path || '').split('/').pop() || a.name || ''; }
// 附件预览/下载 URL（UI kit 内联，零外部依赖；与 app/src/api.js attachmentUrl 同构）
function attUrl(taskId, name, download) {
  return `/api/tasks/${taskId}/attachments/${encodeURIComponent(name)}${download ? '?download=1' : ''}`;
}

// 三来源（选择/粘贴/拖拽）统一去重键：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 [warn, setWarn] = React.useState('');
  // 为图片预生成缩略图 objectURL（幂等，按去重键缓存）——只在 addFiles 调用，render 仅读，避免严格模式下重复 render 泄漏
  const ensureThumb = (f) => {
    if (!f.type || !f.type.startsWith('image/')) return;
    const key = ntFileKey(f);
    const map = urlMapRef.current;
    if (!map.has(key)) map.set(key, URL.createObjectURL(f));
  };
  const thumbUrl = (f) => (f.type && f.type.startsWith('image/')) ? (urlMapRef.current.get(ntFileKey(f)) || null) : null;
  // 组件卸载时释放全部 objectURL
  React.useEffect(() => () => {
    urlMapRef.current.forEach((u) => URL.revokeObjectURL(u));
    urlMapRef.current.clear();
  }, []);
  // 合并新文件并按复合键去重（剪贴板无名图先补名）
  const addFiles = (incoming) => {
    if (!incoming || !incoming.length) return;
    const MAX_BYTES = 25 * 1024 * 1024;  // 单文件软上限，避免超大截图卡住上传
    const named = Array.from(incoming).map((f, i) => ntNamed(f, i));
    const ok = named.filter((f) => f.size <= MAX_BYTES);
    setWarn(ok.length < named.length ? t.attTooLarge : '');
    if (!ok.length) return;
    ok.forEach(ensureThumb);  // 预生成缩略图，render 不再产生副作用
    setFiles((prev) => {
      const seen = new Set(prev.map(ntFileKey));
      const merged = prev.slice();
      ok.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 (
    <form style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: 14, marginBottom: 14 }}
      onSubmit={submit} onPaste={onPaste}>
      <div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
        <span style={{ flex: 1, minWidth: 320, display: 'flex' }}><Input label={t.titleLabel} required placeholder={t.titlePlaceholder} value={title} onChange={setTitle} style={{ width: '100%' }} /></span>
      </div>
      <div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap', marginTop: 12 }}>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
          <span>{t.complexity} <span style={{ color: 'var(--red)' }}>*</span></span>
          <ComplexitySeg defaultValue="auto" includeAuto autoLabel={t.cplxAuto} onChange={setComplexity} />
        </label>
        <Select label={t.priority} defaultValue="1" onChange={setPriority} options={[
          { value: '0', label: t.p0 }, { value: '1', label: t.p1 }, { value: '2', label: t.p2 },
        ]} />
        <span style={{ flex: 1, minWidth: 240, display: 'flex' }}>
          <Select label={t.parentTask} style={{ width: '100%' }} onChange={setParentId} options={[{ value: '', label: t.topLevel }, ...flat]} />
        </span>
      </div>
      <div style={{ marginTop: 12 }}>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 6, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
          <span>{t.attachLabel}</span>
          <div className={'m-nt-drop' + (dragOver ? ' is-over' : '')}
            onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
            onDragEnter={(e) => { e.preventDefault(); setDragOver(true); }}
            onDragLeave={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setDragOver(false); }}
            onDrop={(e) => { e.preventDefault(); addFiles(e.dataTransfer.files); setDragOver(false); }}>
            <input type="file" multiple
              onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }}
              style={{ fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--ink)' }} />
            <span className={'m-nt-hint' + (dragOver ? ' is-over' : '')}>{dragOver ? t.dropHint : t.pasteHint}</span>
            {warn ? <span className="m-nt-hint" style={{ color: 'var(--amber)' }}>{warn}</span> : null}
            {files.length ? (
              <div className="m-nt-grid">
                {files.map((f) => {
                  const key = ntFileKey(f);
                  const url = thumbUrl(f);
                  return (
                    <div key={key} className="m-nt-chip">
                      <button type="button" className="m-nt-del" title={t.removeFile}
                        onClick={() => removeFile(key)}>×</button>
                      <div className="m-nt-thumb">
                        {url ? <img src={url} alt={f.name} /> : <span className="m-nt-ext">{ntExtLabel(f)}</span>}
                      </div>
                      <span className="m-nt-name" title={f.name}>{f.name}</span>
                      <span className="m-nt-size">{(f.size / 1024).toFixed(0)}KB</span>
                    </div>
                  );
                })}
              </div>
            ) : null}
          </div>
        </label>
      </div>
      <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--line-soft)' }}>
        <Button onClick={onCancel}>{t.cancel}</Button>
        <Button variant="solid" type="submit">{t.create}</Button>
      </div>
    </form>
  );
}

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 (
    <section>
      <KitSectionHead icon="tasks" title={t.taskSection} count={byId.size} accent="var(--green)" folded={folded} onToggleFold={toggleFold} t={t} sticky />
      {!folded ? (
        <React.Fragment>
          <FilterBar t={t} kw={kw} setKw={setKw} sets={sets} toggle={toggle} clearOne={clearOne}
            matchCount={matchCount} filtering={filtering} onClear={clearFilters} />
          <div>
            {roots.map((tk) => (
              <TaskRow key={tk.id} task={tk} depth={0} ctx={visibleSet ? visibleSet.ctx.has(tk.id) : false}
                expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
                byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} />
            ))}
          </div>
        </React.Fragment>
      ) : null}
    </section>
  );
}

Object.assign(window, { MaestroKitTaskSection: TaskSection, MaestroKitNewTaskPanel: NewTaskPanel });
