// 真实数据版 App：从本地 daemon 的 REST 拉取、订阅 WS 增量刷新，
// 通过 src/adapt.js 映射成各 surface 期望的形状（替代 window.MAESTRO_MOCK）。
import { createRoot } from 'react-dom/client';
import { api, subscribeEvents } from './api.js';
import {
  adaptProject, buildTaskTree, adaptApproval, adaptEvent, adaptQuota,
  deriveGlobal, deriveAgentSummary, adaptActiveAgents,
} from './adapt.js';

const { Toast } = window.MaestroDesignSystem_a6a290;

const USER = { name: 'local', handle: '@local', plan: 'Claude Code', initial: 'L', hue: 200 };

// 新建项目模态（design 无现成 surface，用 DS 组件就地拼装）。标签 zh/en 自含。
function NewProjectModal({ t, lang, onCreate, onClose, defaultAutonomy }) {
  const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
  const L = lang === 'zh'
    ? { title: '新建项目', name: '项目名', repo: '仓库路径', branch: '默认分支', mode: '工作模式', namePh: 'my-app', repoPh: '/Users/you/code/my-app', create: '创建' }
    : { title: 'New project', name: 'Name', repo: 'Repo path', branch: 'Default branch', mode: 'Mode', namePh: 'my-app', repoPh: '/Users/you/code/my-app', create: 'Create' };
  const [name, setName] = React.useState('');
  const [repoPath, setRepoPath] = React.useState('');
  const [defaultBranch, setDefaultBranch] = React.useState('main');
  const [autonomy, setAutonomy] = React.useState(defaultAutonomy || 'manual');
  const submit = (e) => {
    e.preventDefault();
    if (!name.trim() || !repoPath.trim()) return;
    onCreate({ name: name.trim(), repoPath: repoPath.trim(), defaultBranch: defaultBranch.trim() || 'main', autonomy });
  };
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
      <form onSubmit={submit} style={{ position: 'relative', width: 'min(520px, 92vw)', background: 'var(--panel)', border: '1px solid var(--green-dim)', borderRadius: 'var(--radius-lg,10px)', padding: 18, boxShadow: '0 0 0 1px var(--line-soft), 0 24px 64px rgba(0,0,0,.6)', animation: 'maestro-rise .18s ease both' }}>
        <div style={{ fontWeight: 700, fontSize: 14, letterSpacing: '.06em', marginBottom: 14, color: 'var(--green)' }}>▍ {L.title}</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <Input label={L.name} required placeholder={L.namePh} value={name} onChange={setName} style={{ width: '100%' }} />
          <Input label={L.repo} required placeholder={L.repoPh} value={repoPath} onChange={setRepoPath} style={{ width: '100%' }} />
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <Input label={L.branch} value={defaultBranch} onChange={setDefaultBranch} width={140} />
            <Select label={L.mode} value={autonomy} onChange={setAutonomy} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />
          </div>
        </div>
        <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18, paddingTop: 12, borderTop: '1px solid var(--line-soft)' }}>
          <Button onClick={onClose}>{t.cancel}</Button>
          <Button variant="solid" type="submit">{L.create}</Button>
        </div>
      </form>
    </div>
  );
}

// 工具参数 → 可读摘要（命令 / 文件 / 模式…）
function toolArg(input) {
  if (!input || typeof input !== 'object') return '';
  return input.command || input.file_path || input.path || input.pattern || input.url || input.prompt
    || (Object.keys(input).length ? JSON.stringify(input) : '');
}
const trunc = (s, n) => { s = String(s == null ? '' : s); return s.length > n ? s.slice(0, n) + '…' : s; };

// SDK 流式消息 → 结构化块（助手文本 / 工具调用含参数 / 工具结果预览 / 终态 / 元事件）
function messageBlocks(m) {
  if (!m || !m.type) return [];
  const content = m.message && m.message.content;
  const out = [];
  if (m.type === 'assistant' && Array.isArray(content)) {
    for (const c of content) {
      if (c.type === 'text' && c.text && c.text.trim()) out.push({ kind: 'text', text: c.text.trim() });
      else if (c.type === 'tool_use') out.push({ kind: 'tool', name: c.name, arg: trunc(toolArg(c.input), 220) });
    }
  } else if (m.type === 'user' && Array.isArray(content)) {
    for (const c of content) {
      if (c.type === 'tool_result') {
        const cnt = typeof c.content === 'string' ? c.content
          : Array.isArray(c.content) ? c.content.map((x) => (x && x.text) || '').join('') : '';
        out.push({ kind: 'result', text: trunc((cnt || '').trim(), 400), isError: c.is_error });
      }
    }
  } else if (m.type === 'result') out.push({ kind: 'meta', text: '■ ' + (m.subtype || 'done') });
  else if (typeof m.type === 'string' && m.type.startsWith('maestro.')) out.push({ kind: 'meta', text: '· ' + m.type });
  return out;
}

// 单块渲染：文本 / 工具调用(青) / 结果预览(暗、缩进) / 元事件
function StreamLine({ b }) {
  if (b.kind === 'tool') {
    return <div style={{ padding: '3px 0' }}><span style={{ color: 'var(--cyan)', fontWeight: 600 }}>⚙ {b.name}</span>{b.arg ? <span style={{ color: 'var(--muted)' }}>  {b.arg}</span> : null}</div>;
  }
  if (b.kind === 'result') {
    return <div style={{ padding: '1px 0 5px 16px', color: b.isError ? 'var(--red)' : 'var(--faint)', fontSize: 11.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>↩ {b.text || '(空)'}</div>;
  }
  if (b.kind === 'meta') return <div style={{ padding: '3px 0', color: 'var(--green)' }}>{b.text}</div>;
  return <div style={{ padding: '3px 0', color: 'var(--ink)' }}>{b.text}</div>;
}

// 实时流模态：EventSource 跟随某 run 的 transcript，逐条渲染 agent 输出
function StreamModal({ agent, onClose }) {
  const [lines, setLines] = React.useState([]);
  const [status, setStatus] = React.useState('connecting');
  const boxRef = React.useRef(null);
  React.useEffect(() => {
    const es = new EventSource(`/api/tasks/${agent.taskId}/stream`);
    es.addEventListener('open', () => setStatus('streaming'));
    es.onmessage = (e) => {
      let bs = [];
      try { bs = messageBlocks(JSON.parse(e.data)); } catch { bs = []; }
      if (bs.length) setLines((ls) => [...ls, ...bs].slice(-500));
    };
    es.addEventListener('end', () => { setStatus('done'); es.close(); });
    es.onerror = () => { setStatus('disconnected'); es.close(); };
    return () => es.close();
  }, [agent.taskId]);
  React.useEffect(() => { if (boxRef.current) boxRef.current.scrollTop = boxRef.current.scrollHeight; }, [lines]);
  const dot = { connecting: 'var(--amber)', streaming: 'var(--cyan)', done: 'var(--green)', disconnected: 'var(--faint)' }[status];
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
      <div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(960px,94vw)', height: '82vh', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 'var(--radius-lg,10px)', overflow: 'hidden', boxShadow: '0 0 0 1px var(--line-soft), 0 24px 64px rgba(0,0,0,.6)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)' }}>
          <span style={{ width: 7, height: 7, borderRadius: '50%', background: dot, boxShadow: `0 0 8px ${dot}` }}></span>
          <b style={{ fontSize: 13 }}>{agent.title}</b>
          <span style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', padding: '0 6px', borderRadius: 3 }}>{agent.kind}</span>
          <span style={{ fontSize: 11, color: 'var(--faint)' }}>实时输出 · {status}</span>
          <span style={{ flex: 1 }}></span>
          <button onClick={onClose} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer' }}>✕</button>
        </div>
        <div ref={boxRef} style={{ flex: 1, overflowY: 'auto', padding: '12px 16px', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>
          {lines.length ? lines.map((b, i) => <StreamLine key={i} b={b} />)
            : <div style={{ color: 'var(--faint)' }}>等待输出…</div>}
        </div>
      </div>
    </div>
  );
}

// 接管模态：展示 daemon 准备好的 worktree + 在自己终端起交互式 claude 的命令
function TakeoverModal({ info, onClose }) {
  const [copied, setCopied] = React.useState(false);
  const copy = () => { try { navigator.clipboard.writeText(info.console); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { /* 非安全上下文 */ } };
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
      <div style={{ position: 'relative', width: 'min(620px,92vw)', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 'var(--radius-lg,10px)', padding: 18, boxShadow: '0 0 0 1px var(--line-soft), 0 24px 64px rgba(0,0,0,.6)' }}>
        <div style={{ fontWeight: 700, fontSize: 14, color: 'var(--cyan)', marginBottom: 12 }}>⌨ 人工接管 · {info.title}</div>
        <div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 8 }}>worktree 已就绪。在你自己的终端运行下面命令，起交互式 Claude Code 手动接手：</div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'stretch' }}>
          <code style={{ flex: 1, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--cyan-dim)', borderRadius: 4, padding: '10px 12px', fontSize: 12.5, color: 'var(--ink)', wordBreak: 'break-all' }}>{info.console}</code>
          <button onClick={copy} style={{ flex: 'none', fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer', background: 'transparent', color: copied ? 'var(--green)' : 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 4, padding: '0 12px' }}>{copied ? '✓ 已复制' : '复制'}</button>
        </div>
        <div style={{ fontSize: 11, color: 'var(--faint)', marginTop: 10 }}>分支 {info.branch} · 完成后用 git 提交,可经审核区合并。</div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
          <button onClick={onClose} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: '1px solid var(--line)', borderRadius: 4, padding: '5px 14px', cursor: 'pointer' }}>关闭</button>
        </div>
      </div>
    </div>
  );
}

export function App() {
  const I18N = window.MAESTRO_I18N;
  const [lang, setLang] = React.useState(() => {
    const saved = localStorage.getItem('maestro-kit-lang');
    return I18N[saved] ? saved : 'zh';
  });
  const t = I18N[lang];
  const { Button } = window.MaestroDesignSystem_a6a290;

  // ── 真实数据 state ──────────────────────────────────
  const [projectsRaw, setProjectsRaw] = React.useState([]);
  const [tasksRaw, setTasksRaw] = React.useState([]);
  const [approvalsRaw, setApprovalsRaw] = React.useState([]);
  const [eventsRaw, setEventsRaw] = React.useState([]);
  const [agentsResp, setAgentsResp] = React.useState({ totalActive: 0, agents: [] });
  const [usage, setUsage] = React.useState(null);
  const [settings, setSettings] = React.useState(null);
  const [currentId, setCurrentId] = React.useState(() => localStorage.getItem('maestro-kit-project') || null);
  React.useEffect(() => { if (currentId) localStorage.setItem('maestro-kit-project', currentId); }, [currentId]);
  const [wsState, setWsState] = React.useState('closed');

  // ── UI state（沿用原型）─────────────────────────────
  const [showConfig, setShowConfig] = React.useState(false);
  const [showNew, setShowNew] = React.useState(false);
  const [archiveItem, setArchiveItem] = React.useState(null);
  const [streamAgent, setStreamAgent] = React.useState(null);
  const [takeoverInfo, setTakeoverInfo] = React.useState(null);
  const [toasts, setToasts] = React.useState([]);
  const [theme, setTheme] = React.useState(() => localStorage.getItem('maestro-kit-theme') || 'dark');
  React.useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('maestro-kit-theme', theme);
  }, [theme]);
  React.useEffect(() => { localStorage.setItem('maestro-kit-lang', lang); }, [lang]);
  const [collapsed, setCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-sidebar') === 'collapsed');
  const [evCollapsed, setEvCollapsed] = React.useState(() => localStorage.getItem('maestro-kit-events') === 'collapsed');
  const toggleEvents = () => setEvCollapsed((c) => { localStorage.setItem('maestro-kit-events', c ? 'open' : 'collapsed'); return !c; });
  const toggleCollapse = () => setCollapsed((c) => { localStorage.setItem('maestro-kit-sidebar', c ? 'open' : 'collapsed'); return !c; });
  const SIDE_MIN = 200, SIDE_MAX = 420, EV_MIN = 240, EV_MAX = 520, CENTER_MIN = 480;
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
  const sideMax = () => Math.min(SIDE_MAX, window.innerWidth - (evCollapsed ? 52 : evW) - CENTER_MIN);
  const evMax = () => Math.min(EV_MAX, window.innerWidth - (collapsed ? 52 : sideW) - CENTER_MIN);
  const [sideW, setSideW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-sidew')) || 232, SIDE_MIN, SIDE_MAX));
  const [evW, setEvW] = React.useState(() => clamp(Number(localStorage.getItem('maestro-kit-evw')) || 320, EV_MIN, EV_MAX));
  const [dragging, setDragging] = React.useState(null);
  React.useEffect(() => { localStorage.setItem('maestro-kit-sidew', sideW); }, [sideW]);
  React.useEffect(() => { localStorage.setItem('maestro-kit-evw', evW); }, [evW]);
  const startDrag = (which) => (e) => {
    e.preventDefault();
    setDragging(which);
    const startX = e.clientX;
    const startW = which === 'side' ? sideW : evW;
    const onMove = (ev) => {
      if (which === 'side') setSideW(clamp(startW + (ev.clientX - startX), SIDE_MIN, Math.max(SIDE_MIN, sideMax())));
      else setEvW(clamp(startW - (ev.clientX - startX), EV_MIN, Math.max(EV_MIN, evMax())));
    };
    const onUp = () => {
      setDragging(null);
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      document.body.style.userSelect = '';
    };
    document.body.style.userSelect = 'none';
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  };
  const Resizer = ({ which }) => (
    <div onPointerDown={startDrag(which)} title="拖拽调整宽度"
      style={{
        position: 'absolute', top: 0, bottom: 0, width: 9, cursor: 'col-resize', zIndex: 40,
        ...(which === 'side' ? { left: sideW - 4 } : { right: evW - 4 }),
        display: 'flex', justifyContent: 'center',
      }}>
      <span style={{ width: 1, background: dragging === which ? 'var(--green)' : 'var(--line-soft)', boxShadow: dragging === which ? '0 0 8px var(--green)' : 'none', transition: 'background .12s' }}></span>
    </div>
  );

  // ── 数据加载 ────────────────────────────────────────
  const toast = (kind, text) => {
    const id = Date.now() + Math.random();
    setToasts((list) => [...list, { id, kind, text }]);
    setTimeout(() => setToasts((list) => list.filter((x) => x.id !== id)), 2600);
  };

  const loadGlobal = React.useCallback(async () => {
    try {
      const [pj, ag, us, ap] = await Promise.all([api.listProjects(), api.agents(), api.usage(), api.approvals()]);
      setProjectsRaw(pj); setAgentsResp(ag); setUsage(us); setApprovalsRaw(ap);
      // settings 端点可能未部署到旧 daemon：独立拉取，失败则降级为默认（不阻断主加载）
      api.getSettings().then(setSettings).catch(() => setSettings(null));
      setCurrentId((cur) => (cur && pj.some((p) => p.id === cur) ? cur : (pj[0] && pj[0].id) || null));
    } catch (e) { toast('warn', '加载失败：' + e.message); }
  }, []);

  const loadProject = React.useCallback(async (pid) => {
    if (!pid) return;
    try {
      const [tasks, events] = await Promise.all([api.projectTasks(pid), api.projectEvents(pid)]);
      setTasksRaw(tasks);
      setEventsRaw(events.slice(0, 60));
    } catch (e) { toast('warn', '加载任务失败：' + e.message); }
  }, []);

  // 左侧栏项目拖拽排序：乐观重排 + 落库（reorder 写 sortOrder）
  const reorderProjects = async (order) => {
    setProjectsRaw((prev) => order.map((id) => prev.find((p) => p.id === id)).filter(Boolean));
    try { await api.reorderProjects(order); } catch (e) { toast('warn', '排序保存失败：' + e.message); loadGlobal(); }
  };

  // 用户级全局默认配置：弹框保存 → 落库
  const saveSettings = async (data) => {
    try { const saved = await api.putSettings(data); setSettings(saved); toast('ok', t.toastSaved); }
    catch (e) { toast('warn', '保存失败：' + e.message); }
  };

  React.useEffect(() => { loadGlobal(); }, [loadGlobal]);
  React.useEffect(() => { loadProject(currentId); }, [currentId, loadProject]);

  // WS：增量刷新。事件即时入流；任务/审批/agent 做轻量去抖刷新。
  const refreshTimer = React.useRef(null);
  React.useEffect(() => {
    const unsub = subscribeEvents((evt) => {
      if (!evt || !evt.type) return;
      if (!evt.projectId || evt.projectId === currentId) {
        setEventsRaw((list) => [evt, ...list].slice(0, 60));
      }
      if (evt.type === 'budget.exceeded') {
        const pay = evt.payload || {};
        toast('warn', `⚠ 项目超预算已暂停 · 已用 $${(pay.spend ?? 0).toFixed?.(2) ?? pay.spend} / $${pay.budget}`);
      }
      clearTimeout(refreshTimer.current);
      refreshTimer.current = setTimeout(() => {
        loadProject(currentId);
        api.approvals().then(setApprovalsRaw).catch(() => {});
        api.agents().then(setAgentsResp).catch(() => {});
        api.listProjects().then(setProjectsRaw).catch(() => {});
      }, 350);
    }, setWsState);
    return () => { unsub(); clearTimeout(refreshTimer.current); };
  }, [currentId, loadProject]);

  // ── 派生：映射成 surface 形状 ───────────────────────
  const projects = projectsRaw.map(adaptProject);
  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) || 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, 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);
  const global = deriveGlobal(projects, agentsResp);
  const agentSummary = deriveAgentSummary(projects, agentsResp, usage?.cost);
  const NONTERMINAL = (s) => !['done', 'cancelled', 'decomposed'].includes(s);
  const counts = {
    gate: gates.length,
    ready: tasksRaw.filter((x) => x.projectId === currentId && (x.status === 'ready' || x.status === 'queued')).length,
    run: tasksRaw.filter((x) => x.projectId === currentId && x.status === 'executing').length,
    blocked: tasksRaw.filter((x) => x.projectId === currentId && x.status === 'blocked').length,
    total: tasksRaw.filter((x) => x.projectId === currentId && NONTERMINAL(x.status)).length,
  };

  // ── 动作（真实写入）────────────────────────────────
  const decide = async (a, action, reason) => {
    setApprovalsRaw((list) => list.filter((x) => x.id !== a.id)); // 乐观移除
    try {
      await api.decide(a.id, action, reason);
      toast(action === 'accept' ? 'ok' : 'warn', (action === 'accept' ? t.toastAccepted : t.toastRejected) + (action === 'accept' ? a.title : ''));
    } catch (e) {
      toast('warn', '裁决失败：' + e.message);
      api.approvals().then(setApprovalsRaw).catch(() => {}); // 回滚
    }
  };
  const onSync = async () => {
    try { await api.syncProject(currentId); toast('ok', t.toastSynced); loadProject(currentId); loadGlobal(); }
    catch (e) { toast('warn', '同步失败：' + e.message); }
  };
  const createTask = async (payload) => {
    const { files, ...body } = payload;
    try {
      const task = await api.createTask(currentId, body);
      if (files && files.length) {
        try {
          // 消费后端去重后的权威列表：新建任务原本无附件，stored 即本批去重后入库条数
          const { attachments } = await api.uploadAttachments(task.id, files);
          const stored = attachments.length;
          const deduped = files.length - stored; // >0 表示有同内容文件被去重
          toast('ok', t.attUploaded.replace('{n}', stored) + (deduped > 0 ? t.attDeduped.replace('{n}', deduped) : ''));
        } catch (e) { toast('warn', e.message); }
      }
      toast('ok', t.toastCreated);
      loadProject(currentId); loadGlobal();
    } catch (e) { toast('warn', '创建失败：' + e.message); }
  };
  const saveConfig = async (patch) => {
    try {
      await api.patchProject(currentId, patch);
      setShowConfig(false); toast('ok', t.toastSaved); loadGlobal();
    } catch (e) { toast('warn', '保存失败：' + e.message); }
  };
  const takeover = async (task) => {
    try {
      const info = await api.takeover(task.id);
      setTakeoverInfo({ ...info, title: task.title });
    } catch (e) { toast('warn', '接管失败：' + e.message); }
  };
  // 任务生命周期操作（编辑落库 / 取消 / 重投 / 删除），统一带刷新
  const taskOps = {
    patch: async (taskId, body) => {
      try { await api.patchTask(taskId, body); toast('ok', '已更新'); loadProject(currentId); }
      catch (e) { toast('warn', '更新失败：' + e.message); }
    },
    cancel: async (taskId) => {
      try { await api.cancelTask(taskId); toast('warn', '已取消'); loadProject(currentId); loadGlobal(); }
      catch (e) { toast('warn', '取消失败：' + e.message); }
    },
    requeue: async (taskId) => {
      try { await api.requeueTask(taskId); toast('ok', '已重投'); loadProject(currentId); loadGlobal(); }
      catch (e) { toast('warn', '重投失败：' + e.message); }
    },
    del: async (taskId) => {
      try { await api.deleteTask(taskId); toast('warn', '已删除'); loadProject(currentId); loadGlobal(); }
      catch (e) { toast('warn', '删除失败：' + e.message); }
    },
    // 删除已建任务的单个附件（name = 磁盘文件名 basename(att.path)），成功后局部刷新
    delAttachment: async (taskId, name) => {
      try { await api.deleteAttachment(taskId, name); toast('ok', t.attDeleted); loadProject(currentId); }
      catch (e) { toast('warn', t.attDeleteFailed + e.message); }
    },
  };
  const [showNewProject, setShowNewProject] = React.useState(false);
  const createProject = async (body) => {
    try {
      const p = await api.createProject(body);
      setShowNewProject(false); toast('ok', '项目已创建 · ' + body.name);
      await loadGlobal(); setCurrentId(p.id);
    } catch (e) { toast('warn', '创建项目失败：' + e.message); }
  };

  return (
    <div style={{ position: 'relative', display: 'grid', gridTemplateColumns: (collapsed ? '52px' : sideW + 'px') + ' minmax(0,1fr) ' + (evCollapsed ? '52px' : evW + 'px'), height: '100vh' }}>
      {!collapsed ? <Resizer which="side" /> : null}
      {!evCollapsed ? <Resizer which="ev" /> : null}
      <window.MaestroKitSidebar projects={projects} currentId={currentId} t={t}
        global={global} user={USER} summary={agentSummary} onGlobalConfig={() => toast('warn', t.toastModal)}
        collapsed={collapsed} onToggleCollapse={toggleCollapse}
        onSelect={setCurrentId} onNewProject={() => setShowNewProject(true)} onReorder={reorderProjects}
        settings={settings} onSaveSettings={saveSettings} />
      <main style={{ overflowY: 'auto', minHeight: 0, padding: '0 22px 60px' }}>
        {project ? <window.MaestroKitTopbar project={project} t={t} theme={theme}
          onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
          lang={lang} onSelectLang={setLang}
          counts={counts}
          onSync={onSync}
          onToggleConfig={() => setShowConfig(!showConfig)} /> : null}
        {project ? (
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 14 }}>
            <Button variant="solid" onClick={() => setShowNew((s) => !s)}>{t.newTask}</Button>
          </div>
        ) : null}
        {showNew && project ? <window.MaestroKitNewTaskPanel tasks={tasks} t={t}
          onCancel={() => setShowNew(false)}
          onCreate={(payload) => { setShowNew(false); createTask(payload); }} /> : null}
        {showConfig && project ? <window.MaestroKitConfigPanel project={project} t={t} lang={lang}
          onSave={saveConfig}
          onSync={onSync}
          onClose={() => setShowConfig(false)} /> : null}
        <window.MaestroKitAgentSection agents={agents} quota={quota} t={t} onOpenStream={setStreamAgent} />
        <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} />
        ) : (
          <div style={{ padding: '46px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, marginTop: 18 }}>
            <b style={{ display: 'block', fontSize: 15, color: 'var(--muted)', marginBottom: 6, letterSpacing: '.2em' }}>{t.empty}</b>
            {t.emptyTasks}
          </div>
        )}
        <window.MaestroKitArchiveSection items={archived} t={t} onOpen={setArchiveItem} />
      </main>
      <window.MaestroKitEventPanel events={events} collapsed={evCollapsed} onToggleCollapse={toggleEvents} t={t} />
      {archiveItem ? <window.MaestroKitArchiveModal item={archiveItem} t={t} onClose={() => setArchiveItem(null)} /> : null}
      {showNewProject ? <NewProjectModal t={t} lang={lang} defaultAutonomy={settings?.autonomy} onCreate={createProject} onClose={() => setShowNewProject(false)} /> : null}
      {streamAgent ? <StreamModal agent={streamAgent} onClose={() => setStreamAgent(null)} /> : null}
      {takeoverInfo ? <TakeoverModal info={takeoverInfo} onClose={() => setTakeoverInfo(null)} /> : null}
      <div style={{ position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center' }}>
        {toasts.map((x) => <Toast key={x.id} kind={x.kind}>{x.text}</Toast>)}
      </div>
    </div>
  );
}

export function mount() {
  createRoot(document.getElementById('root')).render(<App />);
}
