From 1c620594241716e64a3173abd16e679eaa6aa940 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 29 Jun 2026 17:42:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=8E=A7=E5=88=B6=E5=8F=B0=20UI?= =?UTF-8?q?=20=E6=89=B9=E6=AC=A1=E2=80=94=E2=80=94=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=B2=BE=E7=AE=80+=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E5=BC=B9=E6=A1=86=E3=80=81=E9=A1=B9=E7=9B=AE=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=B0=81=E9=A1=B6=206=E3=80=81=E8=AF=84=E5=AE=A1=E5=BC=B9?= =?UTF-8?q?=E6=A1=86=E5=A2=9E=E5=BC=BA=E3=80=81live=20=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E6=B5=81=E3=80=81=E6=A0=87=E9=A2=98=E6=A0=8F=E7=BD=AE=E9=A1=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全局 AGENT 卡片精简为 2×2 概览(近 7 天窗口 + 真实运行数),删按项目列表,加「详情」弹框(按天/周/月柱状图 + 分项目下钻表);- 项目列表展开态封顶 6,多余折叠;- plan/exec 评审弹框:markdown 折叠卡片 + 点文件看 diff;- live 输出流提取 tool_use/tool_result;- Agent/审批闸/任务树/已归档标题栏滚动置顶。 Co-Authored-By: Claude Opus 4.8 --- app/src/adapt.js | 29 ++-- app/src/api.js | 3 +- app/src/app.jsx | 67 ++++++-- design/ui_kits/console/ArchiveSection.jsx | 2 +- design/ui_kits/console/GateSection.jsx | 173 ++++++++++++++++++- design/ui_kits/console/Sidebar.jsx | 200 +++++++++++++++++----- design/ui_kits/console/TaskTree.jsx | 2 +- design/ui_kits/console/i18n.js | 15 ++ 8 files changed, 414 insertions(+), 77 deletions(-) diff --git a/app/src/adapt.js b/app/src/adapt.js index 685d7c5..8440b54 100644 --- a/app/src/adapt.js +++ b/app/src/adapt.js @@ -79,15 +79,25 @@ export function buildTaskTree(flat) { // ── 审批闸卡片 ───────────────────────────────────────── export function adaptApproval(tk, projName) { const gate = tk.status.replace('_review', ''); // plan | spec | exec + const r = tk.result || {}; let doc; if (gate === 'plan') doc = tk.plan; else if (gate === 'spec') doc = tk.spec; - else doc = (tk.result && (tk.result.diffSummary || JSON.stringify(tk.result, null, 2))) || tk.operations; - return { + else doc = r.summary || r.diffSummary || tk.operations; // exec:优先 code review 报告(做了什么/怎么做/测试/逐条 review) + const out = { id: tk.id, gate, taskId: tk.id, title: tk.title, meta: `${projName || ''} · ${(tk.complexity || '').toUpperCase()}`, doc: doc || '(无可预览内容)', }; + // 结果评审:挂上 双复审结论 + 安全报告 + 提交 + diff 体量,供评审弹层结构化展示 + if (gate === 'exec' && tk.result) { + out.review = { + verdict: r.verdict || null, securityVerdict: r.securityVerdict || null, + securitySummary: r.securitySummary || null, + commits: r.commits || [], diffSummary: r.diffSummary || null, branch: r.branch || null, + }; + } + return out; } // ── 事件流 ───────────────────────────────────────────── @@ -144,20 +154,13 @@ export function deriveGlobal(projects, agentsResp) { }; } -// 跨项目 agent 概览。token/成本来自 /api/usage 的 cost 明细(按当期窗口聚合)。 +// 跨项目 agent 概览(常驻卡片 2×2 概览)。token/成本/运行来自 /api/usage 的 cost 明细(近 7 天窗口聚合)。 +// 按项目下钻已移至「详情」弹框(独立走 /api/usage/detail),此处不再构建 byProject。 export function deriveAgentSummary(projects, agentsResp, cost) { - const byPid = new Map((cost?.byProject || []).map((c) => [c.projectId, c])); const tokensTotal = (cost?.byProject || []).reduce((sum, c) => sum + (c.tokens || 0), 0); return { - tokensWeek: tokensTotal, runsWeek: 0, costWeek: cost?.total || 0, activeNow: agentsResp?.totalActive ?? 0, - byProject: projects.map((p) => { - const c = byPid.get(p.id); - return { - id: p.id, name: p.name, hue: p.hue, - active: p.agents || 0, runs: 0, - tokens: c?.tokens || 0, cost: c?.costUsd || 0, - }; - }), + tokensWeek: tokensTotal, runsWeek: cost?.runs || 0, costWeek: cost?.total || 0, + activeNow: agentsResp?.totalActive ?? 0, }; } diff --git a/app/src/api.js b/app/src/api.js index 1ef6d6c..7314333 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -24,7 +24,8 @@ export const api = { projectEvents: (pid) => http('GET', `/api/projects/${pid}/events`), agents: () => http('GET', '/api/agents'), approvals: () => http('GET', '/api/approvals'), - usage: () => http('GET', '/api/usage'), + usage: () => http('GET', '/api/usage?period=week'), + usageDetail: (g) => http('GET', `/api/usage/detail?granularity=${g}`), syncProject: (pid) => http('POST', `/api/projects/${pid}/sync`), decide: (taskId, action, reason, merge) => http('POST', `/api/tasks/${taskId}/decide`, { action, reason, merge }), diff --git a/app/src/app.jsx b/app/src/app.jsx index 6397814..2be15e4 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -48,19 +48,47 @@ function NewProjectModal({ t, lang, onCreate, onClose, defaultAutonomy }) { ); } -// SDK 流式消息 → 一行可读文本(助手文本 / 工具调用 / 终态 / maestro 元事件) -function summarizeMessage(m) { - if (!m || !m.type) return ''; +// 工具参数 → 可读摘要(命令 / 文件 / 模式…) +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)) { - return content.map((c) => (c.type === 'text' ? c.text : c.type === 'tool_use' ? `⚙ ${c.name}` : '')).filter(Boolean).join(' '); + 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
⚙ {b.name}{b.arg ? {b.arg} : null}
; } - if (m.type === 'user' && Array.isArray(content)) { - return content.some((c) => c.type === 'tool_result') ? '↩ tool result' : ''; + if (b.kind === 'result') { + return
↩ {b.text || '(空)'}
; } - if (m.type === 'result') return `■ ${m.subtype || 'done'}`; - if (typeof m.type === 'string' && m.type.startsWith('maestro.')) return `· ${m.type}`; - return ''; + if (b.kind === 'meta') return
{b.text}
; + return
{b.text}
; } // 实时流模态:EventSource 跟随某 run 的 transcript,逐条渲染 agent 输出 @@ -72,9 +100,9 @@ function StreamModal({ agent, onClose }) { const es = new EventSource(`/api/tasks/${agent.taskId}/stream`); es.addEventListener('open', () => setStatus('streaming')); es.onmessage = (e) => { - let text = ''; - try { text = summarizeMessage(JSON.parse(e.data)); } catch { text = ''; } - if (text) setLines((ls) => [...ls.slice(-400), text]); + 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(); }; @@ -95,7 +123,7 @@ function StreamModal({ agent, onClose }) {
- {lines.length ? lines.map((l, i) =>
{l}
) + {lines.length ? lines.map((b, i) => ) :
等待输出…
}
@@ -275,7 +303,16 @@ export function App() { const REVIEW = new Set(['plan_review', 'spec_review', 'exec_review']); const gates = approvalsRaw .filter((a) => a.projectId === currentId && REVIEW.has(a.status)) - .map((a) => adaptApproval(a, projNameOf(a.projectId))); + .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 })); + } + return g; + }); const agents = adaptActiveAgents(agentsResp, currentId); const events = eventsRaw.map(adaptEvent); const quota = adaptQuota(usage); @@ -366,7 +403,7 @@ export function App() { collapsed={collapsed} onToggleCollapse={toggleCollapse} onSelect={setCurrentId} onNewProject={() => setShowNewProject(true)} onReorder={reorderProjects} settings={settings} onSaveSettings={saveSettings} /> -
+
{project ? setTheme(theme === 'light' ? 'dark' : 'light')} lang={lang} onSelectLang={setLang} diff --git a/design/ui_kits/console/ArchiveSection.jsx b/design/ui_kits/console/ArchiveSection.jsx index 2de9578..8305004 100644 --- a/design/ui_kits/console/ArchiveSection.jsx +++ b/design/ui_kits/console/ArchiveSection.jsx @@ -103,7 +103,7 @@ function ArchiveSection({ items, t, onOpen }) { const slice = items.slice((cur - 1) * size, cur * size); return (
- + {!folded ? (
{slice.map((it) => )}
diff --git a/design/ui_kits/console/GateSection.jsx b/design/ui_kits/console/GateSection.jsx index 1359fa5..ebf82d4 100644 --- a/design/ui_kits/console/GateSection.jsx +++ b/design/ui_kits/console/GateSection.jsx @@ -1,6 +1,108 @@ +// ── 报告渲染:把 markdown(## 章节 + - 列表 + **粗体** + `代码`)渲染成可折叠卡片 ── +function mdInline(text) { + const nodes = []; const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; + let last = 0; let m; let key = 0; + while ((m = re.exec(text))) { + if (m.index > last) nodes.push(text.slice(last, m.index)); + const tok = m[0]; + if (tok.startsWith('**')) nodes.push({tok.slice(2, -2)}); + else nodes.push({tok.slice(1, -1)}); + last = m.index + tok.length; + } + if (last < text.length) nodes.push(text.slice(last)); + return nodes; +} +function mdBody(body) { + const out = []; let key = 0; + for (const raw of body.split('\n')) { + const t = raw.trim(); + if (!t) { out.push(
); continue; } + if (/^[-*]\s/.test(t)) { + out.push(
·{mdInline(t.replace(/^[-*]\s/, ''))}
); + } else if (/^#{1,4}\s/.test(t)) { + out.push(
{mdInline(t.replace(/^#{1,4}\s/, ''))}
); + } else { + out.push(
{mdInline(t)}
); + } + } + return out; +} +function mdSections(md) { + if (!md) return []; + const out = []; const lines = md.split('\n'); + let cur = null; + for (const line of lines) { + const h = line.match(/^##\s+(.+)$/); + if (h) { if (cur) out.push(cur); cur = { title: h[1].trim(), body: '' }; } + else if (cur) { cur.body += (cur.body ? '\n' : '') + line; } + else { if (line.trim()) { cur = { title: '', body: line }; } } // 首章节前的前言 + } + if (cur) out.push(cur); + return out; +} +function ReportSectionCard({ title, body, t, defaultOpen }) { + const [open, setOpen] = React.useState(defaultOpen); + return ( +
+
setOpen((o) => !o)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '9px 12px', cursor: 'pointer', background: 'var(--panel-2)', userSelect: 'none' }}> + + {title || '说明'} +
+ {open ?
{mdBody(body)}
: null} +
+ ); +} +function ReportCards({ md, t }) { + const secs = mdSections(md); + if (!secs.length) return null; + return
{secs.map((s, i) => )}
; +} + +// 单文件 diff 渲染(+/- 行着色) +function DiffBody({ diff }) { + return ( +
+      {diff.split('\n').map((ln, i) => {
+        const c = (ln.startsWith('+') && !ln.startsWith('+++')) ? 'var(--green)'
+          : (ln.startsWith('-') && !ln.startsWith('---')) ? 'var(--red)'
+          : ln.startsWith('@@') ? 'var(--cyan)'
+          : (ln.startsWith('diff --git') || ln.startsWith('index ') || ln.startsWith('+++') || ln.startsWith('---')) ? 'var(--faint)'
+          : 'var(--muted)';
+        return 
{ln || ' '}
; + })} +
+ ); +} +// 改动文件列表:点击文件展开其 diff(按需向 daemon 取) +function FileDiffs({ taskId, t }) { + const [files, setFiles] = React.useState(null); + const [openPath, setOpenPath] = React.useState(null); + React.useEffect(() => { + let alive = true; + fetch('/api/tasks/' + taskId + '/diff').then((r) => r.json()).then((d) => { if (alive) setFiles(d.files || []); }).catch(() => { if (alive) setFiles([]); }); + return () => { alive = false; }; + }, [taskId]); + if (!files) return
… diff
; + if (!files.length) return null; + return ( +
+ {files.map((f) => ( +
+
setOpenPath((p) => (p === f.path ? null : f.path))} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '7px 11px', cursor: 'pointer', background: 'var(--panel-2)' }}> + + {f.path} + {t.clickToView || ''} +
+ {openPath === f.path ? : null} +
+ ))} +
+ ); +} + // 全屏预览:读完整方案 + 就地裁决 function GatePreview({ item, t, onDecide, onClose }) { - const { ComplexityBadge, Button, Textarea } = window.MaestroDesignSystem_a6a290; + const { ComplexityBadge, StatusChip, Button, Textarea } = window.MaestroDesignSystem_a6a290; const [rejecting, setRejecting] = React.useState(false); const [reason, setReason] = React.useState(''); React.useEffect(() => { @@ -9,6 +111,26 @@ function GatePreview({ item, t, onDecide, onClose }) { return () => window.removeEventListener('keydown', onKey); }, [onClose]); if (!item) return null; + // 拆解子任务序号映射(依赖用 #序号 引用) + const subIdx = {}; + (item.subtasks || []).forEach((s, i) => { subIdx[s.id] = i + 1; }); + const rv = item.review; + const vBadge = (label, v) => { + const ok = v === 'approve', bad = v === 'reject'; + return ( + + {label} + {ok ? '✓ ' + t.vApprove : bad ? '✗ ' + t.vReject : t.vNone} + + ); + }; + const secLabel = (txt) =>
{txt}
; + const reportBox = (txt) =>
{txt}
; return (
@@ -21,8 +143,53 @@ function GatePreview({ item, t, onDecide, onClose }) {
+ {item.gate === 'exec' && rv ? ( +
+ {vBadge(t.lblCodeReview, rv.verdict)} + {vBadge(t.lblSecurity, rv.securityVerdict)} +
+ ) : null} {item.docLabel ?
{item.docLabel}
: null} -
{item.doc}
+ {/^##\s/m.test(item.doc || '') + ? + :
{item.doc}
} + {item.gate === 'exec' && rv && rv.securitySummary ? ( + {secLabel(t.lblSecurity)} + ) : null} + {item.gate === 'exec' && rv && (rv.commits.length || rv.diffSummary) ? ( + + {secLabel(t.lblChanges + (rv.branch ? ' · ' + rv.branch : ''))} +
+ {rv.commits.map((c) =>
· {c}
)} + {rv.diffSummary ?
{rv.diffSummary}
: null} +
+ +
+ ) : null} + {item.gate === 'plan' && item.subtasks && item.subtasks.length ? ( +
+
{t.gateSubtasks} · {item.subtasks.length}
+
+ {item.subtasks.map((s, i) => { + const deps = (s.deps || []).map((d) => (subIdx[d] ? '#' + subIdx[d] : null)).filter(Boolean); + return ( +
+
+ {i + 1} + + P{s.priority} + {s.title} + {StatusChip ? : null} +
+
+ {t.dependsOn}: {deps.length ? {deps.join('、')} : t.noDeps} +
+
+ ); + })} +
+
+ ) : null}
{rejecting ? ( @@ -60,7 +227,7 @@ function GateSection({ approvals, onDecide, t }) { if (approvals.length === 0) return null; return (
- + {!sectionFolded ? : null} {!sectionFolded ? approvals.map((a) => { const isFolded = folded.has(a.id); diff --git a/design/ui_kits/console/Sidebar.jsx b/design/ui_kits/console/Sidebar.jsx index deb5bc6..a6cbec3 100644 --- a/design/ui_kits/console/Sidebar.jsx +++ b/design/ui_kits/console/Sidebar.jsx @@ -231,14 +231,13 @@ function fmtTokens(n) { return String(n); } -// 全局 AGENT 卡片(内联常驻;按项目默认 3 个、多余折叠、按最近活跃排序) -function AgentCard({ global, summary, t, collapsed }) { - const [showAll, setShowAll] = React.useState(false); +function fmtUsd(n) { return '$' + (n || 0).toFixed(2); } +function shortModel(m) { return (m || 'unknown').replace(/^claude-/, ''); } + +// 全局 AGENT 卡片(内联常驻;2×2 概览 = token/花费/运行/活跃,近 7 天窗口;明细走「详情」弹框) +function AgentCard({ global, summary, t, collapsed, projects }) { + const [showDetail, setShowDetail] = 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 (
{t.gAgents.toUpperCase()} {t.apWeek}
- {/* 总结 */} -
+ {/* 概览 2×2 */} +
{[ [fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'], ['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'], @@ -262,40 +261,140 @@ function AgentCard({ global, summary, t, collapsed }) {
))}
- {/* 按项目(默认 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} -
+ {/* 详情按钮 → 弹框 */} + + {showDetail ? setShowDetail(false)} /> : null} +
+ ); +} + +// 用量详情弹框:按天/按月时间序列柱状图 + 分项目下钻表(token/花费/任务数/模型)。直连 /api/usage/detail。 +function AgentDetailModal({ projects, t, onClose }) { + const [gran, setGran] = React.useState('day'); + const [data, setData] = React.useState(null); + const [err, setErr] = React.useState(false); + React.useEffect(() => { + let alive = true; + setData(null); setErr(false); + fetch('/api/usage/detail?granularity=' + gran) + .then((r) => r.ok ? r.json() : Promise.reject(new Error('http'))) + .then((d) => { if (alive) setData(d); }) + .catch(() => { if (alive) setErr(true); }); + return () => { alive = false; }; + }, [gran]); + + const nameOf = new Map((projects || []).map((p) => [p.id, p])); + const series = data?.series || []; + const maxCost = Math.max(...series.map((s) => s.costUsd), 1e-9); + const tot = data?.total || { costUsd: 0, tokens: 0, runs: 0, tasks: 0 }; + const emptyBox = (msg) =>
{msg}
; + + return ( +
+
+
+ {/* 标题 + 段切换 */} +
+ + {t.apDetailTitle} +
+ {[['day', t.apByDay], ['week', t.apByWeek], ['month', t.apByMonth]].map(([g, label]) => ( + + ))}
- ))} +
+ + {err ? emptyBox(t.apEmpty) : !data ? emptyBox('…') : ( + + {/* total 概览 */} +
+ {[ + [fmtUsd(tot.costUsd), t.apCost, 'var(--green)'], + [fmtTokens(tot.tokens), t.apTokens, 'var(--cyan)'], + [tot.runs, t.apRuns, 'var(--ink)'], + [tot.tasks, t.apColTasks, 'var(--ink)'], + ].map(([v, k, c], i) => ( +
+
{v}
+
{k}
+
+ ))} +
+ + {/* 时间序列柱状图(按花费成柱,hover 看 token) */} + {series.length ? ( + +
+ {series.map((s) => ( +
+
+
+ ))} +
+
+ {series[0].bucket}{series[series.length - 1].bucket} +
+
+ ) :
{emptyBox(t.apEmpty)}
} + + {/* 分项目下钻表 */} +
{t.apPerProj}
+ + + + + + + + + + + + {data.byProject.length ? data.byProject.map((p) => { + const pr = nameOf.get(p.projectId); + const nm = pr?.name || p.projectId; + const hue = pr?.hue ?? 200; + return ( + + + + + + + + ); + }) : } + +
{t.apColProject}{t.apTokens}{t.apCost}{t.apColTasks}{t.apColModels}
+ + {nm[0].toUpperCase()} + {nm} + + {fmtTokens(p.tokens)}{fmtUsd(p.costUsd)}{p.tasks} + + {p.models.map((m) => ( + {shortModel(m.model)} + ))} + +
{emptyBox(t.apEmpty)}
+
+ )}
- {projs.length > 3 ? ( - - ) : null}
); } @@ -371,6 +470,10 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog const { Button } = window.MaestroDesignSystem_a6a290; const [dragId, setDragId] = React.useState(null); const [overId, setOverId] = React.useState(null); + const [showAllProjects, setShowAllProjects] = React.useState(false); + const PROJ_LIMIT = 6; + // 展开态下最多显示 6 个,多余折叠(折叠侧栏 rail 态显示全部小圆点,不截断) + const shownProjects = (collapsed || showAllProjects) ? projects : projects.slice(0, PROJ_LIMIT); const onDrop = (targetId) => { if (dragId && dragId !== targetId && onReorder) { const ids = projects.map((x) => x.id); @@ -402,7 +505,7 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog
) :
}
    - {projects.map((p) => { + {shownProjects.map((p) => { const active = p.id === currentId; const meta = projStateMeta(p.state, t); return ( @@ -453,9 +556,20 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog ); })}
+ {!collapsed && projects.length > PROJ_LIMIT ? ( + + ) : null} {/* ── 全局 AGENT 卡片(常驻)+ 用户(全局设置已并入用户菜单)── */}
- +
diff --git a/design/ui_kits/console/TaskTree.jsx b/design/ui_kits/console/TaskTree.jsx index 4f63f0b..4c9a0d3 100644 --- a/design/ui_kits/console/TaskTree.jsx +++ b/design/ui_kits/console/TaskTree.jsx @@ -42,7 +42,7 @@ function KitSectionHead({ icon, title, count, accent = 'var(--green)', folded, o 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: 5, background: 'linear-gradient(var(--bg) 78%, transparent)' } : {}), + ...(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)' } : {}), }}> {title} diff --git a/design/ui_kits/console/i18n.js b/design/ui_kits/console/i18n.js index 929e4ce..9956fae 100644 --- a/design/ui_kits/console/i18n.js +++ b/design/ui_kits/console/i18n.js @@ -24,6 +24,7 @@ window.MAESTRO_I18N = { projPendingTip: '{n} 项待你审批', gAgents: '全局 Agent', gAgentsDetail: '{p}/{P} 个项目运行 · {a} 个 agent', apActive: '运行', apRuns: '次运行', apTokens: 'token', apWeek: '本周', apCost: '花费', apTotal: '所有项目', apPerProj: '按项目', apIdle: '空闲', apMore: '展开其余 {n} 个', apCollapse: '收起', + apDetail: '详情', apDetailTitle: '用量详情', apByDay: '按天', apByWeek: '按周', apByMonth: '按月', apColProject: '项目', apColTasks: '任务数', apColModels: '模型', apEmpty: '暂无用量数据', gConfig: '全局配置', gConfigDetail: '并发上限 {n} · daemon {v}', gUserPlan: '订阅', gUserSignOut: '退出登录', gUserSettings: '账户设置', gGlobalSettings: '全局设置', gSettingsHint: '以下为新建项目的默认值(按用户保存)', gMaxRetries: '最大重试', gTimeoutMin: '超时(分钟)', gAutoPlan: '拆解自动放行', gAutoExec: '执行自动放行', gBudget: '预算 $(空=不限)', gBudgetPeriod: '预算周期', gPeriodDay: '按天', gPeriodMonth: '按月', @@ -48,6 +49,8 @@ window.MAESTRO_I18N = { gatePassed: ' 通过', status: { init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解', decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案', ready: '可执行', blocked: '被依赖阻塞', queued: '排队中', executing: '执行中', exec_review: '待审/合', failed: '失败', needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消' }, gates: { plan: '拆解评审', spec: '方案评审', exec: '结果评审' }, + gateSubtasks: '拆解出的子任务', noDeps: '无依赖', dependsOn: '依赖', + lblCodeReview: '代码复审', lblSecurity: '安全审计', lblChanges: '改动', vApprove: '通过', vReject: '驳回', vNone: '无结论', eventTypes: { 'task.created': '任务创建', 'task.updated': '任务更新', 'status.changed': '状态变更', 'approval.requested': '请求审批', 'approval.granted': '审批通过', 'approval.rejected': '审批驳回', 'run.started': '运行开始', 'run.finished': '运行结束', 'project.synced': 'todo 同步' }, }, en: { @@ -71,6 +74,7 @@ window.MAESTRO_I18N = { projPendingTip: '{n} awaiting your review', gAgents: 'Agents', gAgentsDetail: '{p}/{P} projects · {a} agents', apActive: 'active', apRuns: 'runs', apTokens: 'tokens', apWeek: 'this week', apCost: 'cost', apTotal: 'All projects', apPerProj: 'BY PROJECT', apIdle: 'idle', apMore: '+{n} more', apCollapse: 'Collapse', + apDetail: 'Details', apDetailTitle: 'Usage detail', apByDay: 'By day', apByWeek: 'By week', apByMonth: 'By month', apColProject: 'Project', apColTasks: 'Tasks', apColModels: 'Models', apEmpty: 'No usage data yet', gConfig: 'Settings', gConfigDetail: 'Max concurrency {n} · daemon {v}', gUserPlan: 'Plan', gUserSignOut: 'Sign out', gUserSettings: 'Account settings', gGlobalSettings: 'Global settings', gSettingsHint: 'Defaults applied to new projects (saved per user)', gMaxRetries: 'Max retries', gTimeoutMin: 'Timeout (min)', gAutoPlan: 'Auto-approve plan', gAutoExec: 'Auto-approve exec', gBudget: 'Budget $ (empty=∞)', gBudgetPeriod: 'Budget period', gPeriodDay: 'Daily', gPeriodMonth: 'Monthly', @@ -95,6 +99,8 @@ window.MAESTRO_I18N = { gatePassed: ' approved', status: { init: 'New', analyzing: 'Analyzing', plan_review: 'Plan review', decomposed: 'Decomposed', speccing: 'Drafting spec', spec_review: 'Spec review', ready: 'Ready', blocked: 'Blocked', queued: 'Queued', executing: 'Executing', exec_review: 'Review & merge', failed: 'Failed', needs_attention: 'Needs attention', done: 'Done', paused: 'Paused', cancelled: 'Cancelled' }, gates: { plan: 'PLAN REVIEW', spec: 'SPEC REVIEW', exec: 'EXEC REVIEW' }, + gateSubtasks: 'Decomposed subtasks', noDeps: 'no deps', dependsOn: 'deps', + lblCodeReview: 'Code review', lblSecurity: 'Security', lblChanges: 'Changes', vApprove: 'Approve', vReject: 'Reject', vNone: 'No verdict', eventTypes: { 'task.created': 'Task created', 'task.updated': 'Task updated', 'status.changed': 'Status changed', 'approval.requested': 'Approval requested', 'approval.granted': 'Approval granted', 'approval.rejected': 'Approval rejected', 'run.started': 'Run started', 'run.finished': 'Run finished', 'project.synced': 'Todo synced' }, }, es: { @@ -118,6 +124,7 @@ window.MAESTRO_I18N = { projPendingTip: '{n} esperan tu revisión', gAgents: 'Agents globales', gAgentsDetail: '{p}/{P} proyectos · {a} agents', apActive: 'activos', apRuns: 'ejecuciones', apTokens: 'tokens', apWeek: 'esta semana', apCost: 'coste', apTotal: 'Todos los proyectos', apPerProj: 'POR PROYECTO', apIdle: 'inactivo', apMore: '+{n} más', apCollapse: 'Contraer', + apDetail: 'Detalles', apDetailTitle: 'Detalle de uso', apByDay: 'Por día', apByWeek: 'Por semana', apByMonth: 'Por mes', apColProject: 'Proyecto', apColTasks: 'Tareas', apColModels: 'Modelos', apEmpty: 'Sin datos de uso', gConfig: 'Config global', gConfigDetail: 'Concurrencia máx {n} · daemon {v}', gUserPlan: 'Plan', gUserSignOut: 'Cerrar sesión', gUserSettings: 'Ajustes de cuenta', gGlobalSettings: 'Configuración global', gSettingsHint: 'Valores por defecto para proyectos nuevos (por usuario)', gMaxRetries: 'Reintentos máx.', gTimeoutMin: 'Tiempo límite (min)', gAutoPlan: 'Auto-aprobar plan', gAutoExec: 'Auto-aprobar ejec.', gBudget: 'Presupuesto $ (vacío=∞)', gBudgetPeriod: 'Periodo', gPeriodDay: 'Diario', gPeriodMonth: 'Mensual', @@ -142,6 +149,8 @@ window.MAESTRO_I18N = { gatePassed: ' aprobada', status: { init: 'Nueva', analyzing: 'Analizando', plan_review: 'Revisión de plan', decomposed: 'Descompuesta', speccing: 'Redactando spec', spec_review: 'Revisión de spec', ready: 'Lista', blocked: 'Bloqueada', queued: 'En cola', executing: 'Ejecutando', exec_review: 'Revisar y fusionar', failed: 'Fallida', needs_attention: 'Requiere atención', done: 'Hecha', paused: 'Pausada', cancelled: 'Cancelada' }, gates: { plan: 'REVISIÓN DE PLAN', spec: 'REVISIÓN DE SPEC', exec: 'REVISIÓN DE RESULTADO' }, + gateSubtasks: 'Subtareas', noDeps: 'sin deps', dependsOn: 'deps', + lblCodeReview: 'Revisión', lblSecurity: 'Seguridad', lblChanges: 'Cambios', vApprove: 'Aprobar', vReject: 'Rechazar', vNone: 'Sin veredicto', eventTypes: { 'task.created': 'Tarea creada', 'task.updated': 'Tarea actualizada', 'status.changed': 'Cambio de estado', 'approval.requested': 'Aprobación solicitada', 'approval.granted': 'Aprobación concedida', 'approval.rejected': 'Aprobación rechazada', 'run.started': 'Run iniciado', 'run.finished': 'Run terminado', 'project.synced': 'Todo sincronizado' }, }, ja: { @@ -165,6 +174,7 @@ window.MAESTRO_I18N = { projPendingTip: '{n} 件があなたの承認待ち', gAgents: 'グローバル Agent', gAgentsDetail: '{p}/{P} プロジェクト · {a} 個の agent', apActive: '実行中', apRuns: '回実行', apTokens: 'token', apWeek: '今週', apCost: 'コスト', apTotal: '全プロジェクト', apPerProj: 'プロジェクト別', apIdle: 'アイドル', apMore: '他 {n} 件', apCollapse: '折りたたむ', + apDetail: '詳細', apDetailTitle: '使用量の詳細', apByDay: '日別', apByWeek: '週別', apByMonth: '月別', apColProject: 'プロジェクト', apColTasks: 'タスク数', apColModels: 'モデル', apEmpty: '使用量データなし', gConfig: 'グローバル設定', gConfigDetail: '並列上限 {n} · daemon {v}', gUserPlan: 'プラン', gUserSignOut: 'ログアウト', gUserSettings: 'アカウント設定', gGlobalSettings: 'グローバル設定', gSettingsHint: '新規プロジェクトの既定値(ユーザー単位で保存)', gMaxRetries: '最大リトライ', gTimeoutMin: 'タイムアウト(分)', gAutoPlan: '分解を自動承認', gAutoExec: '実行を自動承認', gBudget: '予算 $(空=無制限)', gBudgetPeriod: '予算期間', gPeriodDay: '日次', gPeriodMonth: '月次', @@ -189,6 +199,8 @@ window.MAESTRO_I18N = { gatePassed: ' 承認', status: { init: '新規', analyzing: '分析・分解中', plan_review: '分解確認待ち', decomposed: '分解済み', speccing: '方針作成中', spec_review: '方針確認待ち', ready: '実行可能', blocked: '依存ブロック', queued: 'キュー待ち', executing: '実行中', exec_review: 'レビュー/マージ', failed: '失敗', needs_attention: '要対応', done: '完了', paused: '一時停止', cancelled: 'キャンセル' }, gates: { plan: '分解レビュー', spec: '方針レビュー', exec: '結果レビュー' }, + gateSubtasks: '分解されたサブタスク', noDeps: '依存なし', dependsOn: '依存', + lblCodeReview: 'コードレビュー', lblSecurity: 'セキュリティ', lblChanges: '変更', vApprove: '承認', vReject: '却下', vNone: '判定なし', eventTypes: { 'task.created': 'タスク作成', 'task.updated': 'タスク更新', 'status.changed': 'ステータス変更', 'approval.requested': '承認リクエスト', 'approval.granted': '承認', 'approval.rejected': '却下', 'run.started': 'run 開始', 'run.finished': 'run 終了', 'project.synced': 'todo 同期' }, }, fr: { @@ -212,6 +224,7 @@ window.MAESTRO_I18N = { projPendingTip: '{n} en attente de votre revue', gAgents: 'Agents globaux', gAgentsDetail: '{p}/{P} projets · {a} agents', apActive: 'actifs', apRuns: 'exécutions', apTokens: 'tokens', apWeek: 'cette semaine', apCost: 'coût', apTotal: 'Tous les projets', apPerProj: 'PAR PROJET', apIdle: 'inactif', apMore: '+{n} de plus', apCollapse: 'Réduire', + apDetail: 'Détails', apDetailTitle: "Détail d'usage", apByDay: 'Par jour', apByWeek: 'Par semaine', apByMonth: 'Par mois', apColProject: 'Projet', apColTasks: 'Tâches', apColModels: 'Modèles', apEmpty: "Aucune donnée d'usage", gConfig: 'Config globale', gConfigDetail: 'Concurrence max {n} · daemon {v}', gUserPlan: 'Forfait', gUserSignOut: 'Se déconnecter', gUserSettings: 'Paramètres du compte', gGlobalSettings: 'Paramètres globaux', gSettingsHint: 'Valeurs par défaut des nouveaux projets (par utilisateur)', gMaxRetries: 'Essais max', gTimeoutMin: 'Délai (min)', gAutoPlan: 'Auto-approuver plan', gAutoExec: 'Auto-approuver exéc.', gBudget: 'Budget $ (vide=∞)', gBudgetPeriod: 'Période', gPeriodDay: 'Quotidien', gPeriodMonth: 'Mensuel', @@ -236,6 +249,8 @@ window.MAESTRO_I18N = { gatePassed: ' approuvée', status: { init: 'Nouvelle', analyzing: 'Analyse', plan_review: 'Revue du plan', decomposed: 'Décomposée', speccing: 'Rédaction spec', spec_review: 'Revue de spec', ready: 'Prête', blocked: 'Bloquée', queued: 'En file', executing: 'En exécution', exec_review: 'Revue & fusion', failed: 'Échouée', needs_attention: 'À traiter', done: 'Terminée', paused: 'En pause', cancelled: 'Annulée' }, gates: { plan: 'REVUE DU PLAN', spec: 'REVUE DE SPEC', exec: 'REVUE DU RÉSULTAT' }, + gateSubtasks: 'Sous-tâches', noDeps: 'sans deps', dependsOn: 'deps', + lblCodeReview: 'Revue', lblSecurity: 'Sécurité', lblChanges: 'Modifs', vApprove: 'Approuver', vReject: 'Rejeter', vNone: 'Sans verdict', eventTypes: { 'task.created': 'Tâche créée', 'task.updated': 'Tâche mise à jour', 'status.changed': "Changement d'état", 'approval.requested': 'Approbation demandée', 'approval.granted': 'Approbation accordée', 'approval.rejected': 'Approbation rejetée', 'run.started': 'Run démarré', 'run.finished': 'Run terminé', 'project.synced': 'Todo synchronisé' }, }, };