From 8765e614986b7ada0266fa8b94431da8891c8286 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 29 Jun 2026 11:30:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(console):=20=E4=BB=BB=E5=8A=A1=E6=A0=91?= =?UTF-8?q?=E7=AD=9B=E9=80=89/=E6=8A=98=E5=8F=A0/=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=A0=87=E9=A2=98=20+=20=E4=BE=A7=E6=A0=8F=E5=85=A8=E5=B1=80Ag?= =?UTF-8?q?ent=E5=8D=A1=E7=89=87/=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F/?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E8=AE=BE=E7=BD=AE=E5=BC=B9=E6=A1=86/?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E9=A1=B9=E7=9B=AE=20+=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=82=B9=E7=9C=9F=E5=AE=9E=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 任务树:四维多选下拉筛选(状态/复杂度/优先级/执行者)、已选置顶、新建任务按钮移右上角(0任务也能建)。 四块主区(Agent/审批闸/任务树/归档)统一标题组件(图标+名称+数量徽标)+ 折叠按钮(localStorage 记忆)。 侧栏:全局 Agent 弹层改常驻卡片(项目默认3+折叠+按活跃排序)、顶部项目拖拽排序、全局设置移入用户菜单弹框(保存/取消,落库)、记忆当前项目。 adaptProject 用后端 summary 的 running/attention 判状态点颜色(青/琥珀/灰)+ 紫色关注数徽标;agent 标题不再越界;i18n 5 语言补全。 Co-Authored-By: Claude Opus 4.8 --- app/src/adapt.js | 8 +- app/src/api.js | 3 + app/src/app.jsx | 39 +++- design/ui_kits/console/ArchiveSection.jsx | 16 +- design/ui_kits/console/GateSection.jsx | 14 +- design/ui_kits/console/Sidebar.jsx | 254 +++++++++++++++------- design/ui_kits/console/TaskTree.jsx | 250 ++++++++++++++------- design/ui_kits/console/Topbar.jsx | 10 +- design/ui_kits/console/i18n.js | 30 ++- 9 files changed, 430 insertions(+), 194 deletions(-) diff --git a/app/src/adapt.js b/app/src/adapt.js index cb1e299..685d7c5 100644 --- a/app/src/adapt.js +++ b/app/src/adapt.js @@ -19,13 +19,13 @@ export function adaptProject(p) { const s = p.summary || {}; let state = 'idle'; if (p.status === 'paused') state = 'paused'; - else if (s.executing > 0) state = 'running'; - else if (s.blocked > 0) state = 'blocked'; - else if (s.alive > 0) state = 'running'; + else if (s.running > 0) state = 'running'; // agent 在跑(executor/planner)→ 青点 + else if (s.blocked > 0) state = 'blocked'; // 被依赖阻塞 → 琥珀点 + // 其余 idle(灰点);待审批/需人工数走下面 pending 紫徽标 return { id: p.id, name: p.name, path: shortPath(p.repoPath), branch: p.defaultBranch, autonomy: p.autonomy, concurrency: p.concurrency, hue: hueFor(p.name), - state, pending: s.pending || 0, agents: s.executing || 0, + state, pending: s.attention || 0, agents: s.executing || 0, // 透传给 ConfigPanel 用的原始字段 model: p.model, verifyCmd: p.verifyCmd, maxRetries: p.maxRetries, repoPath: p.repoPath, logo: p.logo, budgetUsd: p.budgetUsd ?? null, budgetPeriod: p.budgetPeriod ?? 'month', diff --git a/app/src/api.js b/app/src/api.js index 9bcbb72..1ef6d6c 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -30,7 +30,10 @@ export const api = { http('POST', `/api/tasks/${taskId}/decide`, { action, reason, merge }), createTask: (pid, body) => http('POST', `/api/projects/${pid}/tasks`, body), patchProject: (pid, body) => http('PATCH', `/api/projects/${pid}`, body), + reorderProjects: (order) => http('POST', '/api/projects/reorder', { order }), createProject: (body) => http('POST', '/api/projects', body), + getSettings: () => http('GET', '/api/settings'), + putSettings: (data) => http('PUT', '/api/settings', data), takeover: (taskId) => http('POST', `/api/tasks/${taskId}/takeover`), patchTask: (taskId, body) => http('PATCH', `/api/tasks/${taskId}`, body), cancelTask: (taskId) => http('POST', `/api/tasks/${taskId}/cancel`), diff --git a/app/src/app.jsx b/app/src/app.jsx index 874fef5..6397814 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -12,7 +12,7 @@ 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 }) { +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: '创建' } @@ -20,7 +20,7 @@ function NewProjectModal({ t, lang, onCreate, onClose }) { const [name, setName] = React.useState(''); const [repoPath, setRepoPath] = React.useState(''); const [defaultBranch, setDefaultBranch] = React.useState('main'); - const [autonomy, setAutonomy] = React.useState('manual'); + const [autonomy, setAutonomy] = React.useState(defaultAutonomy || 'manual'); const submit = (e) => { e.preventDefault(); if (!name.trim() || !repoPath.trim()) return; @@ -133,6 +133,7 @@ export function App() { return I18N[saved] ? saved : 'zh'; }); const t = I18N[lang]; + const { Button } = window.MaestroDesignSystem_a6a290; // ── 真实数据 state ────────────────────────────────── const [projectsRaw, setProjectsRaw] = React.useState([]); @@ -141,11 +142,14 @@ export function App() { const [eventsRaw, setEventsRaw] = React.useState([]); const [agentsResp, setAgentsResp] = React.useState({ totalActive: 0, agents: [] }); const [usage, setUsage] = React.useState(null); - const [currentId, setCurrentId] = 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); @@ -210,7 +214,9 @@ export function App() { 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); - setCurrentId((cur) => cur || (pj[0] && pj[0].id) || null); + // 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); } }, []); @@ -223,6 +229,18 @@ export function App() { } 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]); @@ -346,7 +364,8 @@ export function App() { toast('warn', t.toastModal)} collapsed={collapsed} onToggleCollapse={toggleCollapse} - onSelect={setCurrentId} onNewProject={() => setShowNewProject(true)} /> + onSelect={setCurrentId} onNewProject={() => setShowNewProject(true)} onReorder={reorderProjects} + settings={settings} onSaveSettings={saveSettings} />
{project ? setTheme(theme === 'light' ? 'dark' : 'light')} @@ -354,6 +373,14 @@ export function App() { counts={counts} onSync={onSync} onToggleConfig={() => setShowConfig(!showConfig)} /> : null} + {project ? ( +
+ +
+ ) : null} + {showNew && project ? setShowNew(false)} + onCreate={(payload) => { setShowNew(false); createTask(payload); }} /> : null} {showConfig && project ? {archiveItem ? setArchiveItem(null)} /> : null} - {showNewProject ? setShowNewProject(false)} /> : null} + {showNewProject ? setShowNewProject(false)} /> : null} {streamAgent ? setStreamAgent(null)} /> : null} {takeoverInfo ? setTakeoverInfo(null)} /> : null}
diff --git a/design/ui_kits/console/ArchiveSection.jsx b/design/ui_kits/console/ArchiveSection.jsx index 99e8f5d..2de9578 100644 --- a/design/ui_kits/console/ArchiveSection.jsx +++ b/design/ui_kits/console/ArchiveSection.jsx @@ -95,17 +95,21 @@ function ArchivePager({ page, pages, size, setPage, setSize, t }) { function ArchiveSection({ items, t, onOpen }) { const [page, setPage] = React.useState(1); const [size, setSize] = React.useState(20); + const [folded, setFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-archive') === '1'); + const toggleFold = () => setFolded((f) => { localStorage.setItem('maestro-kit-fold-archive', f ? '0' : '1'); return !f; }); if (!items.length) return null; const pages = Math.max(1, Math.ceil(items.length / size)); const cur = Math.min(page, pages); const slice = items.slice((cur - 1) * size, cur * size); return ( -
-
- {t.archive} · {items.length} -
-
{slice.map((it) => )}
- { setSize(n); setPage(1); }} t={t} /> +
+ + {!folded ? ( + +
{slice.map((it) => )}
+ { setSize(n); setPage(1); }} t={t} /> +
+ ) : null}
); } diff --git a/design/ui_kits/console/GateSection.jsx b/design/ui_kits/console/GateSection.jsx index ad301b6..1359fa5 100644 --- a/design/ui_kits/console/GateSection.jsx +++ b/design/ui_kits/console/GateSection.jsx @@ -47,20 +47,22 @@ function GatePreview({ item, t, onDecide, onClose }) { // 审批闸区:斜纹条 + GateCard 列表(accept/reject + 驳回必填理由) function GateSection({ approvals, onDecide, t }) { - const { GateCard, GateStripe, Button, Textarea, SectionHead } = window.MaestroDesignSystem_a6a290; + const { GateCard, GateStripe, Button, Textarea } = window.MaestroDesignSystem_a6a290; const [rejecting, setRejecting] = React.useState(null); const [reason, setReason] = React.useState(''); const [preview, setPreview] = React.useState(null); + const [sectionFolded, setSectionFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-gate') === '1'); + const toggleSectionFold = () => setSectionFolded((f) => { localStorage.setItem('maestro-kit-fold-gate', f ? '0' : '1'); return !f; }); const [folded, setFolded] = React.useState(() => new Set()); const toggleFold = (id) => setFolded((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); if (approvals.length === 0) return null; return ( -
- - - {approvals.map((a) => { +
+ + {!sectionFolded ? : null} + {!sectionFolded ? approvals.map((a) => { const isFolded = folded.has(a.id); return (
@@ -93,7 +95,7 @@ function GateSection({ approvals, onDecide, t }) {
); - })} + }) : null} setPreview(null)} />
); diff --git a/design/ui_kits/console/Sidebar.jsx b/design/ui_kits/console/Sidebar.jsx index 85d4bf2..deb5bc6 100644 --- a/design/ui_kits/console/Sidebar.jsx +++ b/design/ui_kits/console/Sidebar.jsx @@ -87,8 +87,84 @@ const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke function IcoAgents() { return ; } function IcoGlobalCfg() { return ; } -function UserRow({ user, t, collapsed }) { +// 全局设置弹框:用户级默认配置(新建项目套用)。保存 / 取消,落库经 onSave。 +function SettingsModal({ settings, onSave, onClose, t }) { + const { Button, Input, Select } = window.MaestroDesignSystem_a6a290; + const s = settings || {}; + const [autonomy, setAutonomy] = React.useState(s.autonomy || 'manual'); + const [concurrency, setConcurrency] = React.useState(String(s.concurrency ?? 1)); + const [maxRetries, setMaxRetries] = React.useState(String(s.maxRetries ?? 2)); + const [timeoutMin, setTimeoutMin] = React.useState(String(Math.round((s.timeoutMs ?? 1800000) / 60000))); + const [autoPlan, setAutoPlan] = React.useState(!!s.autoApprovePlan); + const [autoExec, setAutoExec] = React.useState(!!s.autoApproveExec); + const [budgetUsd, setBudgetUsd] = React.useState(s.budgetUsd != null ? String(s.budgetUsd) : ''); + const [budgetPeriod, setBudgetPeriod] = React.useState(s.budgetPeriod || 'month'); + const [model, setModel] = React.useState(s.model || ''); + React.useEffect(() => { + const onKey = (e) => { if (e.key === 'Escape') onClose(); }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + const submit = () => { + onSave({ + autonomy, concurrency: Number(concurrency) || 1, maxRetries: Number(maxRetries) || 0, + timeoutMs: Math.max(1, Number(timeoutMin) || 30) * 60000, + autoApprovePlan: autoPlan, autoApproveExec: autoExec, + budgetUsd: budgetUsd.trim() === '' ? null : Number(budgetUsd), + budgetPeriod, model: model.trim() === '' ? null : model.trim(), + }); + onClose(); + }; + const Toggle = ({ on, set }) => ( + + ); + const field = (label, node) => ( + + ); + return ( +
+
+
+
+ {t.gGlobalSettings} +
+
{t.gSettingsHint}
+
+ {field(t.workMode, )} + {field(t.gMaxRetries, )} + {field(t.gTimeoutMin, )} + {field(t.gAutoPlan, )} + {field(t.gAutoExec, )} + {field(t.gBudget, )} + {field(t.gBudgetPeriod, )} +
+
+ + +
+
+
+ ); +} + +function UserRow({ user, t, collapsed, settings, onSaveSettings }) { const [open, setOpen] = React.useState(false); + const [showSettings, setShowSettings] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; @@ -133,13 +209,18 @@ function UserRow({ user, t, collapsed }) {
{user.name} {user.handle}
{t.gUserPlan} · {user.plan}
- {[t.gUserSettings, t.gUserSignOut].map((label, i) => ( -
{ setShowSettings(true); setOpen(false); }, red: false }, + { label: t.gUserSettings, onClick: null, red: false }, + { label: t.gUserSignOut, onClick: null, red: true }, + ].map((it) => ( +
{ e.currentTarget.style.background = 'var(--panel-2)'; }} - onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{label}
+ onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{it.label}
))} ) : null} + {showSettings ? setShowSettings(false)} t={t} /> : null} ); } @@ -150,83 +231,70 @@ function fmtTokens(n) { return String(n); } -function AgentsRow({ global, summary, t, collapsed }) { - const [open, setOpen] = React.useState(false); - const [pos, setPos] = React.useState(null); - const ref = React.useRef(null); - const rowRef = React.useRef(null); - React.useEffect(() => { - if (!open) return; - const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); }; - document.addEventListener('mousedown', onDoc); - return () => document.removeEventListener('mousedown', onDoc); - }, [open]); - const toggle = () => { - if (!open && rowRef.current) { - const r = rowRef.current.getBoundingClientRect(); - const W = 286; - const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8); - setPos({ left, bottom: window.innerHeight - r.top + 6, width: W }); - } - setOpen((o) => !o); - }; - const maxTok = Math.max(...summary.byProject.map((p) => p.tokens), 1); - const detail = t.gAgentsDetail.replace('{p}', global.runningProjects).replace('{P}', global.projectCount).replace('{a}', global.runningAgents); +// 全局 AGENT 卡片(内联常驻;按项目默认 3 个、多余折叠、按最近活跃排序) +function AgentCard({ global, summary, t, collapsed }) { + const [showAll, setShowAll] = React.useState(false); + if (collapsed) return null; + // 「最近活跃」排序:运行中优先,其次本周 token 用量(无活跃时间戳时的代理) + const projs = [...summary.byProject].sort((a, b) => (b.active - a.active) || (b.tokens - a.tokens)); + const maxTok = Math.max(...projs.map((p) => p.tokens), 1); + const shown = showAll ? projs : projs.slice(0, 3); return ( -
-
- } accent="var(--cyan)" onClick={toggle} - label={t.gAgents} detail={detail} title={t.gAgents + ' · ' + detail} /> +
+
+ {t.gAgents.toUpperCase()} + {t.apWeek}
- {open && pos ? ( -
-
- {t.gAgents.toUpperCase()} - {t.apWeek} + {/* 总结 */} +
+ {[ + [fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'], + ['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'], + [summary.runsWeek, t.apRuns, 'var(--ink)'], + [summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'], + ].map(([v, k, c], i) => ( +
+
{v}
+
{k}
- {/* 总结 */} -
- {[ - [fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'], - ['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'], - [summary.runsWeek, t.apRuns, 'var(--ink)'], - [summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'], - ].map(([v, k, c], i) => ( -
-
{v}
-
{k}
-
- ))} + ))} +
+ {/* 按项目(默认 3,多余折叠,按活跃排序)*/} +
{t.apPerProj}
+
+ {shown.map((p) => ( +
+
+ {p.name[0].toUpperCase()} + {p.name} + {p.active > 0 ? ( + + {p.active} + + ) : {t.apIdle}} + {fmtTokens(p.tokens)} +
+
+ + + + {p.runs} {t.apRuns} +
- {/* 按项目 */} -
{t.apPerProj}
-
- {summary.byProject.map((p) => ( -
-
- {p.name[0].toUpperCase()} - {p.name} - {p.active > 0 ? ( - - {p.active} - - ) : {t.apIdle}} - {fmtTokens(p.tokens)} -
-
- - - - {p.runs} {t.apRuns} -
-
- ))} -
-
+ ))} +
+ {projs.length > 3 ? ( + ) : null}
); @@ -299,8 +367,19 @@ function GlobalConfigRow({ global, t, collapsed }) { ); } -function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig }) { +function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig, onReorder, settings, onSaveSettings }) { const { Button } = window.MaestroDesignSystem_a6a290; + const [dragId, setDragId] = React.useState(null); + const [overId, setOverId] = React.useState(null); + const onDrop = (targetId) => { + if (dragId && dragId !== targetId && onReorder) { + const ids = projects.map((x) => x.id); + const from = ids.indexOf(dragId); + const to = ids.indexOf(targetId); + if (from > -1 && to > -1) { ids.splice(to, 0, ids.splice(from, 1)[0]); onReorder(ids); } + } + setDragId(null); setOverId(null); + }; return (