feat(console): 任务树筛选/折叠/统一标题 + 侧栏全局Agent卡片/拖拽排序/全局设置弹框/记忆项目 + 状态点真实化
任务树:四维多选下拉筛选(状态/复杂度/优先级/执行者)、已选置顶、新建任务按钮移右上角(0任务也能建)。 四块主区(Agent/审批闸/任务树/归档)统一标题组件(图标+名称+数量徽标)+ 折叠按钮(localStorage 记忆)。 侧栏:全局 Agent 弹层改常驻卡片(项目默认3+折叠+按活跃排序)、顶部项目拖拽排序、全局设置移入用户菜单弹框(保存/取消,落库)、记忆当前项目。 adaptProject 用后端 summary 的 running/attention 判状态点颜色(青/琥珀/灰)+ 紫色关注数徽标;agent 标题不再越界;i18n 5 语言补全。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -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',
|
||||
|
||||
@@ -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`),
|
||||
|
||||
+33
-6
@@ -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() {
|
||||
<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)} />
|
||||
onSelect={setCurrentId} onNewProject={() => setShowNewProject(true)} onReorder={reorderProjects}
|
||||
settings={settings} onSaveSettings={saveSettings} />
|
||||
<main style={{ overflowY: 'auto', padding: '0 22px 60px' }}>
|
||||
{project ? <window.MaestroKitTopbar project={project} t={t} theme={theme}
|
||||
onToggleTheme={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
@@ -354,6 +373,14 @@ export function App() {
|
||||
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}
|
||||
@@ -372,7 +399,7 @@ export function App() {
|
||||
</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} onCreate={createProject} onClose={() => setShowNewProject(false)} /> : 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' }}>
|
||||
|
||||
@@ -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 (
|
||||
<section style={{ marginTop: 28, opacity: .82 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.22em', color: 'var(--muted)', textTransform: 'uppercase', padding: '18px 0 10px' }}>
|
||||
<span style={{ color: 'var(--faint)' }}>▣</span>{t.archive} · {items.length}
|
||||
</div>
|
||||
<div>{slice.map((it) => <ArchiveRow key={it.id} item={it} t={t} onOpen={onOpen} />)}</div>
|
||||
<ArchivePager page={cur} pages={pages} size={size} setPage={setPage} setSize={(n) => { setSize(n); setPage(1); }} t={t} />
|
||||
<section style={{ opacity: .82 }}>
|
||||
<window.MaestroKitSectionHead icon="archive" title={t.archive} count={items.length} accent="var(--faint)" folded={folded} onToggleFold={toggleFold} t={t} />
|
||||
{!folded ? (
|
||||
<React.Fragment>
|
||||
<div>{slice.map((it) => <ArchiveRow key={it.id} item={it} t={t} onOpen={onOpen} />)}</div>
|
||||
<ArchivePager page={cur} pages={pages} size={size} setPage={setPage} setSize={(n) => { setSize(n); setPage(1); }} t={t} />
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<section style={{ marginTop: 18 }}>
|
||||
<GateStripe />
|
||||
<SectionHead mark="violet" title={t.gateSection} style={{ paddingTop: 12, color: 'var(--violet)' }} />
|
||||
{approvals.map((a) => {
|
||||
<section>
|
||||
<window.MaestroKitSectionHead icon="gate" title={t.gateSection} count={approvals.length} accent="var(--violet)" folded={sectionFolded} onToggleFold={toggleSectionFold} t={t} />
|
||||
{!sectionFolded ? <GateStripe /> : null}
|
||||
{!sectionFolded ? approvals.map((a) => {
|
||||
const isFolded = folded.has(a.id);
|
||||
return (
|
||||
<div key={a.id} style={{ marginBottom: 12 }}>
|
||||
@@ -93,7 +95,7 @@ function GateSection({ approvals, onDecide, t }) {
|
||||
</GateCard>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
}) : null}
|
||||
<GatePreview item={preview} t={t} onDecide={onDecide} onClose={() => setPreview(null)} />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -87,8 +87,84 @@ const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke
|
||||
function IcoAgents() { return <svg {...sIco}><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" /></svg>; }
|
||||
function IcoGlobalCfg() { return <svg {...sIco}><circle cx="12" cy="12" r="9" /><line x1="3" y1="12" x2="21" y2="12" /><path d="M12 3a14 14 0 0 0 0 18a14 14 0 0 0 0-18" /></svg>; }
|
||||
|
||||
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 }) => (
|
||||
<button type="button" onClick={() => set(!on)} style={{
|
||||
alignSelf: 'flex-start', fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 700, letterSpacing: '.08em',
|
||||
background: on ? 'rgba(95,221,125,.1)' : 'var(--bg-deep)', color: on ? 'var(--green)' : 'var(--faint)',
|
||||
border: '1px solid ' + (on ? 'var(--green-dim)' : 'var(--line)'), borderRadius: 'var(--radius-sm,4px)',
|
||||
padding: '6px 14px', cursor: 'pointer', transition: 'all .1s',
|
||||
}}>{on ? 'ON' : 'OFF'}</button>
|
||||
);
|
||||
const field = (label, node) => (
|
||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.05em' }}>
|
||||
<span>{label}</span>{node}
|
||||
</label>
|
||||
);
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 2000, 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: 460, maxWidth: '92vw', maxHeight: '88vh', overflowY: 'auto',
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
boxShadow: '0 18px 50px rgba(0,0,0,.7)', padding: '18px 20px', animation: 'maestro-rise .14s ease both',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: 700, letterSpacing: '.16em', color: 'var(--muted)', textTransform: 'uppercase', marginBottom: 3 }}>
|
||||
<span style={{ color: 'var(--green)' }}>▍</span>{t.gGlobalSettings}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--faint)', marginBottom: 15 }}>{t.gSettingsHint}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 13 }}>
|
||||
{field(t.workMode, <Select value={autonomy} onChange={setAutonomy} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />)}
|
||||
{field(t.maxConcurrency, <Input type="number" value={concurrency} onChange={setConcurrency} style={{ width: '100%' }} />)}
|
||||
{field(t.gMaxRetries, <Input type="number" value={maxRetries} onChange={setMaxRetries} style={{ width: '100%' }} />)}
|
||||
{field(t.gTimeoutMin, <Input type="number" value={timeoutMin} onChange={setTimeoutMin} style={{ width: '100%' }} />)}
|
||||
{field(t.gAutoPlan, <Toggle on={autoPlan} set={setAutoPlan} />)}
|
||||
{field(t.gAutoExec, <Toggle on={autoExec} set={setAutoExec} />)}
|
||||
{field(t.gBudget, <Input type="number" placeholder="∞" value={budgetUsd} onChange={setBudgetUsd} style={{ width: '100%' }} />)}
|
||||
{field(t.gBudgetPeriod, <Select value={budgetPeriod} onChange={setBudgetPeriod} options={[{ value: 'day', label: t.gPeriodDay }, { value: 'month', label: t.gPeriodMonth }]} />)}
|
||||
</div>
|
||||
<div style={{ marginTop: 13 }}>
|
||||
{field(t.model, <Input placeholder={t.modelPh} value={model} onChange={setModel} style={{ width: '100%' }} />)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18 }}>
|
||||
<Button onClick={onClose}>{t.cancel}</Button>
|
||||
<Button variant="solid" onClick={submit}>{t.save}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 }) {
|
||||
<div style={{ fontSize: 12, fontWeight: 600 }}>{user.name} <span style={{ color: 'var(--faint)', fontWeight: 400 }}>{user.handle}</span></div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--green)', marginTop: 2 }}>{t.gUserPlan} · {user.plan}</div>
|
||||
</div>
|
||||
{[t.gUserSettings, t.gUserSignOut].map((label, i) => (
|
||||
<div key={label} style={{ padding: '6px 10px', fontSize: 12, color: i === 1 ? 'var(--red)' : 'var(--ink)', cursor: 'pointer', borderRadius: 3 }}
|
||||
{[
|
||||
{ label: t.gGlobalSettings, onClick: () => { setShowSettings(true); setOpen(false); }, red: false },
|
||||
{ label: t.gUserSettings, onClick: null, red: false },
|
||||
{ label: t.gUserSignOut, onClick: null, red: true },
|
||||
].map((it) => (
|
||||
<div key={it.label} onClick={it.onClick || undefined} style={{ padding: '6px 10px', fontSize: 12, color: it.red ? 'var(--red)' : 'var(--ink)', cursor: 'pointer', borderRadius: 3 }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-2)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{label}</div>
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{it.label}</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{showSettings ? <SettingsModal settings={settings} onSave={onSaveSettings} onClose={() => setShowSettings(false)} t={t} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={rowRef}>
|
||||
<SideRow collapsed={collapsed} icon={<IcoAgents />} accent="var(--cyan)" onClick={toggle}
|
||||
label={t.gAgents} detail={detail} title={t.gAgents + ' · ' + detail} />
|
||||
<div style={{
|
||||
margin: '10px 12px 4px', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
background: 'var(--bg-deep)', padding: '11px 12px',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 10 }}>
|
||||
<span style={{ color: 'var(--cyan)', display: 'inline-flex' }}><IcoAgents /></span>{t.gAgents.toUpperCase()}
|
||||
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
|
||||
</div>
|
||||
{open && pos ? (
|
||||
<div ref={ref} style={{
|
||||
position: 'fixed', left: pos.left, bottom: pos.bottom, width: pos.width, zIndex: 1000,
|
||||
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
|
||||
boxShadow: '0 12px 34px rgba(0,0,0,.7)', padding: '12px 14px', animation: 'maestro-rise .12s ease both',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 10 }}>
|
||||
<span style={{ color: 'var(--cyan)' }}>▍</span>{t.gAgents.toUpperCase()}
|
||||
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
|
||||
{/* 总结 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 11 }}>
|
||||
{[
|
||||
[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) => (
|
||||
<div key={i} style={{ background: 'var(--bg-deep)', padding: '7px 9px' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
|
||||
<div style={{ fontSize: 9.5, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
|
||||
</div>
|
||||
{/* 总结 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 12 }}>
|
||||
{[
|
||||
[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) => (
|
||||
<div key={i} style={{ background: 'var(--bg-deep)', padding: '8px 10px' }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
{/* 按项目(默认 3,多余折叠,按活跃排序)*/}
|
||||
<div style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--faint)', marginBottom: 7 }}>{t.apPerProj}</div>
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{shown.map((p) => (
|
||||
<div key={p.id}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
|
||||
<span style={{ flex: 'none', width: 16, height: 16, borderRadius: 4, display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + p.hue + ' 45% 60%)' }}>{p.name[0].toUpperCase()}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
{p.active > 0 ? (
|
||||
<span style={{ flex: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9.5, color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 5px' }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 6px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>{p.active}
|
||||
</span>
|
||||
) : <span style={{ flex: 'none', fontSize: 9.5, color: 'var(--faint)' }}>{t.apIdle}</span>}
|
||||
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 11, fontWeight: 600, color: 'var(--cyan)' }}>{fmtTokens(p.tokens)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingLeft: 24 }}>
|
||||
<span style={{ flex: 1, height: 4, background: 'var(--panel-2)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<span style={{ display: 'block', height: '100%', width: (p.tokens / maxTok * 100) + '%', background: 'var(--cyan)', boxShadow: '0 0 6px rgba(89,200,216,.5)' }}></span>
|
||||
</span>
|
||||
<span style={{ flex: 'none', fontSize: 10, color: 'var(--faint)' }}>{p.runs} {t.apRuns}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 按项目 */}
|
||||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.18em', color: 'var(--faint)', marginBottom: 7 }}>{t.apPerProj}</div>
|
||||
<div style={{ display: 'grid', gap: 9 }}>
|
||||
{summary.byProject.map((p) => (
|
||||
<div key={p.id}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
|
||||
<span style={{ flex: 'none', width: 16, height: 16, borderRadius: 4, display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + p.hue + ' 45% 60%)' }}>{p.name[0].toUpperCase()}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
{p.active > 0 ? (
|
||||
<span style={{ flex: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9.5, color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 5px' }}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 6px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>{p.active}
|
||||
</span>
|
||||
) : <span style={{ flex: 'none', fontSize: 9.5, color: 'var(--faint)' }}>{t.apIdle}</span>}
|
||||
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 11, fontWeight: 600, color: 'var(--cyan)' }}>{fmtTokens(p.tokens)}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingLeft: 24 }}>
|
||||
<span style={{ flex: 1, height: 4, background: 'var(--panel-2)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<span style={{ display: 'block', height: '100%', width: (p.tokens / maxTok * 100) + '%', background: 'var(--cyan)', boxShadow: '0 0 6px rgba(89,200,216,.5)' }}></span>
|
||||
</span>
|
||||
<span style={{ flex: 'none', fontSize: 10, color: 'var(--faint)' }}>{p.runs} {t.apRuns}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{projs.length > 3 ? (
|
||||
<button type="button" onClick={() => setShowAll((s) => !s)} style={{
|
||||
marginTop: 9, width: '100%', background: 'transparent', border: '1px dashed var(--line)', color: 'var(--faint)',
|
||||
borderRadius: 4, padding: '4px 0', fontSize: 10.5, fontFamily: 'var(--mono)', cursor: 'pointer', letterSpacing: '.05em',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--muted)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--faint)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
|
||||
{showAll ? t.apCollapse : t.apMore.replace('{n}', projs.length - 3)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -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 (
|
||||
<aside style={{ background: 'var(--bg-deep)', borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
<div style={{ padding: collapsed ? '16px 0 12px' : '20px 16px 14px', borderBottom: '1px solid var(--line-soft)', display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'flex-start' }}>
|
||||
@@ -328,16 +407,24 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog
|
||||
const meta = projStateMeta(p.state, t);
|
||||
return (
|
||||
<li key={p.id} onClick={() => onSelect(p.id)} title={p.name + ' · ' + meta.label + (p.pending ? ' · ' + t.projPendingTip.replace('{n}', p.pending) : '')}
|
||||
draggable={!collapsed}
|
||||
onDragStart={(e) => { setDragId(p.id); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={(e) => { if (dragId) { e.preventDefault(); if (p.id !== dragId && overId !== p.id) setOverId(p.id); } }}
|
||||
onDragLeave={() => setOverId((o) => (o === p.id ? null : o))}
|
||||
onDrop={(e) => { e.preventDefault(); onDrop(p.id); }}
|
||||
onDragEnd={() => { setDragId(null); setOverId(null); }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: 'pointer',
|
||||
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: dragId ? 'grabbing' : 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 9,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
borderLeft: '2px solid ' + (active ? 'var(--green)' : 'transparent'),
|
||||
background: active ? 'var(--panel-2)' : 'transparent',
|
||||
color: active ? 'var(--green)' : 'var(--muted)',
|
||||
opacity: dragId === p.id ? 0.4 : 1,
|
||||
boxShadow: overId === p.id ? 'inset 0 2px 0 var(--green)' : 'none',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseEnter={(e) => { if (!active && !dragId) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
|
||||
<ProjectLogo project={p} size={collapsed ? 26 : 24} />
|
||||
{!collapsed ? (
|
||||
@@ -366,12 +453,11 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* ── 底部:全局 agent · 全局配置 · 用户 ── */}
|
||||
{/* ── 全局 AGENT 卡片(常驻)+ 用户(全局设置已并入用户菜单)── */}
|
||||
<div style={{ borderTop: '1px solid var(--line-soft)' }}>
|
||||
<AgentsRow global={global} summary={summary} t={t} collapsed={collapsed} />
|
||||
<GlobalConfigRow global={global} t={t} collapsed={collapsed} />
|
||||
<AgentCard global={global} summary={summary} t={t} collapsed={collapsed} />
|
||||
</div>
|
||||
<UserRow user={user} t={t} collapsed={collapsed} />
|
||||
<UserRow user={user} t={t} collapsed={collapsed} settings={settings} onSaveSettings={onSaveSettings} />
|
||||
<div style={{ borderTop: '1px solid var(--line-soft)', padding: collapsed ? '10px 0' : '10px 12px', fontSize: 11, color: 'var(--muted)', display: 'flex', alignItems: 'center', gap: 7, justifyContent: collapsed ? 'center' : 'flex-start' }}>
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -1,12 +1,63 @@
|
||||
// 任务树:筛选栏(搜索+复杂度+状态分组)+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情;新建任务表单
|
||||
const KIT_FILTER_GROUPS = [
|
||||
['todo', ['init', 'ready', 'blocked']],
|
||||
['doing', ['analyzing', 'speccing', 'queued', 'executing', 'decomposed']],
|
||||
['gate', ['plan_review', 'spec_review', 'exec_review']],
|
||||
['bad', ['failed', 'needs_attention']],
|
||||
['hold', ['paused', 'cancelled']],
|
||||
['done', ['done']],
|
||||
// 状态全序(下拉选项顺序:待办 → 进行 → 容器 → 审批 → 异常 → 挂起 → 终态)
|
||||
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: 5, background: 'linear-gradient(var(--bg) 78%, transparent)' } : {}),
|
||||
}}>
|
||||
<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); }
|
||||
@@ -53,49 +104,94 @@ function CplxPicker({ task, cplx, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FilterBar({ t, kw, setKw, cplxSet, toggleCplx, statusSet, toggleGroup, matchCount, filtering, onClear }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
const chip = (on, color, dim, label, onClick) => (
|
||||
<button key={label} onClick={onClick} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 700, letterSpacing: '.1em',
|
||||
background: on ? 'rgba(95,221,125,.08)' : 'transparent',
|
||||
color: on ? color : 'var(--faint)',
|
||||
border: '1px solid ' + (on ? dim : 'var(--line)'),
|
||||
borderRadius: 3, padding: '2px 8px', cursor: 'pointer', transition: 'all .1s', whiteSpace: 'nowrap',
|
||||
}}>{label}</button>
|
||||
);
|
||||
// 多选下拉:触发器显示「标签 ·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 (
|
||||
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px', marginBottom: 12, display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
<div style={{ 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: 190, padding: '4px 9px',
|
||||
background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-sm, 4px)', outline: 'none',
|
||||
}} />
|
||||
<span style={{ display: 'inline-flex', gap: 6 }}>
|
||||
{chip(cplxSet.has('hard'), 'var(--red)', 'var(--red-dim)', 'HARD', () => toggleCplx('hard'))}
|
||||
{chip(cplxSet.has('medium'), 'var(--amber)', 'var(--amber-dim)', 'MED', () => toggleCplx('medium'))}
|
||||
{chip(cplxSet.has('easy'), 'var(--green)', 'var(--green-dim)', 'EASY', () => toggleCplx('easy'))}
|
||||
<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>
|
||||
<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>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{KIT_FILTER_GROUPS.map(([g, sts]) => {
|
||||
const on = sts.every((s) => statusSet.has(s));
|
||||
const part = !on && sts.some((s) => statusSet.has(s));
|
||||
return (
|
||||
<button key={g} onClick={() => toggleGroup(sts)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '.04em',
|
||||
background: 'transparent',
|
||||
color: on ? 'var(--green)' : part ? 'var(--amber)' : 'var(--muted)',
|
||||
border: '1px dashed ' + (on ? 'var(--green-dim)' : part ? 'var(--amber-dim)' : 'var(--line-soft)'),
|
||||
borderRadius: 3, padding: '2px 9px', cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
}}>{t.fgroups[g]}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : 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>
|
||||
);
|
||||
}
|
||||
@@ -311,13 +407,15 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
}
|
||||
|
||||
function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const { Button, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||||
const [expandedId, setExpandedId] = React.useState(null);
|
||||
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
|
||||
const [showNew, setShowNew] = React.useState(false);
|
||||
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 [cplxSet, setCplxSet] = React.useState(() => new Set());
|
||||
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]);
|
||||
@@ -329,25 +427,25 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const toggleOpen = (id) => setOpenIds((prev) => {
|
||||
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
|
||||
});
|
||||
const toggleCplx = (v) => setCplxSet((prev) => {
|
||||
// 四维多选筛选: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 toggleGroup = (sts) => setStatusSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
const allOn = sts.every((s) => next.has(s));
|
||||
for (const s of sts) allOn ? next.delete(s) : next.add(s);
|
||||
return next;
|
||||
});
|
||||
const filtering = kw.trim() !== '' || cplxSet.size > 0 || statusSet.size > 0;
|
||||
const clearFilters = () => { setKw(''); setCplxSet(new Set()); setStatusSet(new Set()); };
|
||||
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)) &&
|
||||
(statusSet.size === 0 || statusSet.has(tk.status));
|
||||
(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;
|
||||
@@ -360,7 +458,7 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
for (const tk of tasks) walk(tk);
|
||||
const set = new Set(vis); set.ctx = ctx;
|
||||
return { visibleSet: set, matchCount: count };
|
||||
}, [filtering, kw, cplxSet, statusSet, tasks, cplxOverride]);
|
||||
}, [filtering, kw, statusSet, cplxSet, prioSet, assigneeSet, tasks, cplxOverride]);
|
||||
|
||||
// 依赖跳转:展开所有祖先 + flash 定位
|
||||
const onJump = (id) => {
|
||||
@@ -384,20 +482,22 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const roots = visibleSet ? tasks.filter((tk) => visibleSet.has(tk.id)) : tasks;
|
||||
return (
|
||||
<section>
|
||||
<SectionHead title={t.taskSection} sticky action={<Button variant="ghost" size="xs" onClick={() => setShowNew(!showNew)}>{t.newTask}</Button>} />
|
||||
{showNew ? <NewTaskPanel tasks={tasks} t={t} onCancel={() => setShowNew(false)}
|
||||
onCreate={(payload) => { setShowNew(false); onCreate ? onCreate(payload) : onToast('ok', t.toastCreated); }} /> : null}
|
||||
<FilterBar t={t} kw={kw} setKw={setKw} cplxSet={cplxSet} toggleCplx={toggleCplx}
|
||||
statusSet={statusSet} toggleGroup={toggleGroup} 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>
|
||||
<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 });
|
||||
Object.assign(window, { MaestroKitTaskSection: TaskSection, MaestroKitNewTaskPanel: NewTaskPanel });
|
||||
|
||||
@@ -155,10 +155,13 @@ function ConfigPanel({ project, onSave, onClose, onSync, t, lang }) {
|
||||
}
|
||||
|
||||
function AgentSection({ agents, quota, t, onOpenStream }) {
|
||||
const { SectionHead, QuotaMeter } = window.MaestroDesignSystem_a6a290;
|
||||
const { QuotaMeter } = window.MaestroDesignSystem_a6a290;
|
||||
const [folded, setFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-agent') === '1');
|
||||
const toggleFold = () => setFolded((f) => { localStorage.setItem('maestro-kit-fold-agent', f ? '0' : '1'); return !f; });
|
||||
return (
|
||||
<section>
|
||||
<SectionHead mark="cyan" title={<span>{t.agentSection}<span style={{ color: 'var(--cyan)', letterSpacing: '.08em', marginLeft: 8 }}>{agents.length > 0 ? agents.length : ''}</span></span>} sticky />
|
||||
<window.MaestroKitSectionHead icon="agent" title={t.agentSection} count={agents.length} accent="var(--cyan)" folded={folded} onToggleFold={toggleFold} t={t} sticky />
|
||||
{!folded ? (<React.Fragment>
|
||||
{quota ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 22, flexWrap: 'wrap', fontFamily: 'var(--mono)', margin: '0 0 12px' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.08em' }}>{t.quota}</span>
|
||||
@@ -182,7 +185,7 @@ function AgentSection({ agents, quota, t, onOpenStream }) {
|
||||
onMouseEnter={(e) => { if (onOpenStream && a.taskId) e.currentTarget.style.background = 'var(--panel-2)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
|
||||
<span style={{ flex: 'none', width: 6, height: 6, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 8px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.title}</span>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.title}</span>
|
||||
<span style={{ flex: 'none', fontSize: 10, letterSpacing: '.1em', color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', padding: '0 6px', borderRadius: 3 }}>{a.kind}</span>
|
||||
<span style={{ flex: 'none', marginLeft: 'auto', fontSize: 10.5, color: 'var(--faint)' }}>{a.meta} · {a.time} {t.since}</span>
|
||||
</div>
|
||||
@@ -190,6 +193,7 @@ function AgentSection({ agents, quota, t, onOpenStream }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,16 +23,18 @@ window.MAESTRO_I18N = {
|
||||
projRunning: '运行中', projPaused: '暂停', projBlocked: '阻塞', projIdle: '空闲',
|
||||
projPendingTip: '{n} 项待你审批',
|
||||
gAgents: '全局 Agent', gAgentsDetail: '{p}/{P} 个项目运行 · {a} 个 agent',
|
||||
apActive: '运行', apRuns: '次运行', apTokens: 'token', apWeek: '本周', apCost: '花费', apTotal: '所有项目', apPerProj: '按项目', apIdle: '空闲',
|
||||
apActive: '运行', apRuns: '次运行', apTokens: 'token', apWeek: '本周', apCost: '花费', apTotal: '所有项目', apPerProj: '按项目', apIdle: '空闲', apMore: '展开其余 {n} 个', apCollapse: '收起',
|
||||
gConfig: '全局配置', gConfigDetail: '并发上限 {n} · daemon {v}',
|
||||
gUserPlan: '订阅', gUserSignOut: '退出登录', gUserSettings: '账户设置',
|
||||
gGlobalSettings: '全局设置', gSettingsHint: '以下为新建项目的默认值(按用户保存)', gMaxRetries: '最大重试', gTimeoutMin: '超时(分钟)', gAutoPlan: '拆解自动放行', gAutoExec: '执行自动放行', gBudget: '预算 $(空=不限)', gBudgetPeriod: '预算周期', gPeriodDay: '按天', gPeriodMonth: '按月',
|
||||
gateExpand: '展开', gateCollapse: '折叠', gateDblTip: '双击全屏阅读',
|
||||
quota: '额度', quotaWeek: '周', quotaReset: '{t} 后重置',
|
||||
projLogo: '项目 Logo', logoPh: '图片 URL 或仓库内相对路径;留空=自动', verifyCmd: '校验命令', verifyPh: 'npm test · go test ./...(可空)', model: '模型', modelPh: 'claude-opus-4-5(可空)',
|
||||
searchPh: '⌕ 搜索标题…', clearFilter: '✕ 清除筛选', matchCount: '{n} 项匹配',
|
||||
fgroups: { todo: '待办', doing: '进行中', gate: '待审批', bad: '异常', hold: '挂起', done: '完成' },
|
||||
fStatus: '状态', fAssignee: '执行者', assignees: { agent: '智能体', human: '人工' },
|
||||
fullRead: '全屏阅读', depsLabel: 'DEPS · 依赖(全部完成才可执行)', depDone: '完成', depWait: '等待',
|
||||
gateSection: '审批闸 · 等待裁决', accept: '✓ 通过', reject: '✕ 驳回', confirmReject: '确认驳回',
|
||||
gateSection: '审批闸', accept: '✓ 通过', reject: '✕ 驳回', confirmReject: '确认驳回',
|
||||
rejectPlaceholder: '改进意见(必填)',
|
||||
taskSection: '任务树', newTask: '+ 新建任务', titleLabel: '标题', titlePlaceholder: '要做什么',
|
||||
complexity: '复杂度', cplxAuto: '智能', parentTask: '父任务', topLevel: '(顶层)', priority: '优先级',
|
||||
@@ -68,16 +70,18 @@ window.MAESTRO_I18N = {
|
||||
projRunning: 'Running', projPaused: 'Paused', projBlocked: 'Blocked', projIdle: 'Idle',
|
||||
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',
|
||||
apActive: 'active', apRuns: 'runs', apTokens: 'tokens', apWeek: 'this week', apCost: 'cost', apTotal: 'All projects', apPerProj: 'BY PROJECT', apIdle: 'idle', apMore: '+{n} more', apCollapse: 'Collapse',
|
||||
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',
|
||||
gateExpand: 'Expand', gateCollapse: 'Collapse', gateDblTip: 'Double-click for fullscreen',
|
||||
quota: 'Quota', quotaWeek: 'week', quotaReset: 'resets in {t}',
|
||||
projLogo: 'Project logo', logoPh: 'Image URL or repo-relative path; empty = auto', verifyCmd: 'Verify command', verifyPh: 'npm test · go test ./... (optional)', model: 'Model', modelPh: 'claude-opus-4-5 (optional)',
|
||||
searchPh: '⌕ Search titles…', clearFilter: '✕ Clear filters', matchCount: '{n} matches',
|
||||
fgroups: { todo: 'To do', doing: 'In progress', gate: 'Pending review', bad: 'Issues', hold: 'On hold', done: 'Done' },
|
||||
fStatus: 'Status', fAssignee: 'Assignee', assignees: { agent: 'Agent', human: 'Human' },
|
||||
fullRead: 'Read fullscreen', depsLabel: 'DEPS (all must complete to run)', depDone: 'done', depWait: 'waiting',
|
||||
gateSection: 'Approval gates · awaiting decision', accept: '✓ Accept', reject: '✕ Reject', confirmReject: 'Confirm reject',
|
||||
gateSection: 'Approval gates', accept: '✓ Accept', reject: '✕ Reject', confirmReject: 'Confirm reject',
|
||||
rejectPlaceholder: 'Improvement feedback (required)',
|
||||
taskSection: 'Task tree', newTask: '+ New task', titleLabel: 'Title', titlePlaceholder: 'What needs doing',
|
||||
complexity: 'Complexity', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(top level)', priority: 'Priority',
|
||||
@@ -113,16 +117,18 @@ window.MAESTRO_I18N = {
|
||||
projRunning: 'En curso', projPaused: 'Pausado', projBlocked: 'Bloqueado', projIdle: 'Inactivo',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
gateExpand: 'Expandir', gateCollapse: 'Plegar', gateDblTip: 'Doble clic para pantalla completa',
|
||||
quota: 'Cuota', quotaWeek: 'semana', quotaReset: 'reinicio en {t}',
|
||||
projLogo: 'Logo del proyecto', logoPh: 'URL o ruta relativa; vacío = auto', verifyCmd: 'Comando de verificación', verifyPh: 'npm test · go test ./... (opcional)', model: 'Modelo', modelPh: 'claude-opus-4-5 (opcional)',
|
||||
searchPh: '⌕ Buscar títulos…', clearFilter: '✕ Limpiar filtros', matchCount: '{n} coincidencias',
|
||||
fgroups: { todo: 'Pendientes', doing: 'En curso', gate: 'Por revisar', bad: 'Incidencias', hold: 'En pausa', done: 'Hechas' },
|
||||
fStatus: 'Estado', fAssignee: 'Ejecutor', assignees: { agent: 'Agente', human: 'Humano' },
|
||||
fullRead: 'Pantalla completa', depsLabel: 'DEPS (todas deben completarse)', depDone: 'hecha', depWait: 'esperando',
|
||||
gateSection: 'Puertas de aprobación · pendientes', accept: '✓ Aceptar', reject: '✕ Rechazar', confirmReject: 'Confirmar rechazo',
|
||||
gateSection: 'Puertas de aprobación', accept: '✓ Aceptar', reject: '✕ Rechazar', confirmReject: 'Confirmar rechazo',
|
||||
rejectPlaceholder: 'Comentario de mejora (obligatorio)',
|
||||
taskSection: 'Árbol de tareas', newTask: '+ Nueva tarea', titleLabel: 'Título', titlePlaceholder: 'Qué hay que hacer',
|
||||
complexity: 'Complejidad', cplxAuto: 'AUTO', parentTask: 'Padre', topLevel: '(raíz)', priority: 'Prioridad',
|
||||
@@ -158,16 +164,18 @@ window.MAESTRO_I18N = {
|
||||
projRunning: '実行中', projPaused: '一時停止', projBlocked: 'ブロック', projIdle: 'アイドル',
|
||||
projPendingTip: '{n} 件があなたの承認待ち',
|
||||
gAgents: 'グローバル Agent', gAgentsDetail: '{p}/{P} プロジェクト · {a} 個の agent',
|
||||
apActive: '実行中', apRuns: '回実行', apTokens: 'token', apWeek: '今週', apCost: 'コスト', apTotal: '全プロジェクト', apPerProj: 'プロジェクト別', apIdle: 'アイドル',
|
||||
apActive: '実行中', apRuns: '回実行', apTokens: 'token', apWeek: '今週', apCost: 'コスト', apTotal: '全プロジェクト', apPerProj: 'プロジェクト別', apIdle: 'アイドル', apMore: '他 {n} 件', apCollapse: '折りたたむ',
|
||||
gConfig: 'グローバル設定', gConfigDetail: '並列上限 {n} · daemon {v}',
|
||||
gUserPlan: 'プラン', gUserSignOut: 'ログアウト', gUserSettings: 'アカウント設定',
|
||||
gGlobalSettings: 'グローバル設定', gSettingsHint: '新規プロジェクトの既定値(ユーザー単位で保存)', gMaxRetries: '最大リトライ', gTimeoutMin: 'タイムアウト(分)', gAutoPlan: '分解を自動承認', gAutoExec: '実行を自動承認', gBudget: '予算 $(空=無制限)', gBudgetPeriod: '予算期間', gPeriodDay: '日次', gPeriodMonth: '月次',
|
||||
gateExpand: '展開', gateCollapse: '折り畳む', gateDblTip: 'ダブルクリックで全画面',
|
||||
quota: 'クォータ', quotaWeek: '週', quotaReset: '{t} 後にリセット',
|
||||
projLogo: 'プロジェクトロゴ', logoPh: '画像 URL またはリポジトリ相対パス;空=自動', verifyCmd: '検証コマンド', verifyPh: 'npm test · go test ./...(任意)', model: 'モデル', modelPh: 'claude-opus-4-5(任意)',
|
||||
searchPh: '⌕ タイトルを検索…', clearFilter: '✕ フィルタをクリア', matchCount: '{n} 件一致',
|
||||
fgroups: { todo: '未着手', doing: '進行中', gate: '承認待ち', bad: '異常', hold: '保留', done: '完了' },
|
||||
fStatus: 'ステータス', fAssignee: '担当', assignees: { agent: 'エージェント', human: '人手' },
|
||||
fullRead: '全画面で読む', depsLabel: 'DEPS · 依存(全完了で実行可)', depDone: '完了', depWait: '待機',
|
||||
gateSection: '承認ゲート · 裁定待ち', accept: '✓ 承認', reject: '✕ 却下', confirmReject: '却下を確定',
|
||||
gateSection: '承認ゲート', accept: '✓ 承認', reject: '✕ 却下', confirmReject: '却下を確定',
|
||||
rejectPlaceholder: '改善フィードバック(必須)',
|
||||
taskSection: 'タスクツリー', newTask: '+ 新規タスク', titleLabel: 'タイトル', titlePlaceholder: '何をしますか',
|
||||
complexity: '複雑度', cplxAuto: '智能', parentTask: '親タスク', topLevel: '(トップ)', priority: '優先度',
|
||||
@@ -203,16 +211,18 @@ window.MAESTRO_I18N = {
|
||||
projRunning: 'En cours', projPaused: 'En pause', projBlocked: 'Bloqué', projIdle: 'Inactif',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
gateExpand: 'Déplier', gateCollapse: 'Replier', gateDblTip: 'Double-clic pour le plein écran',
|
||||
quota: 'Quota', quotaWeek: 'semaine', quotaReset: 'reset dans {t}',
|
||||
projLogo: 'Logo du projet', logoPh: 'URL ou chemin relatif ; vide = auto', verifyCmd: 'Commande de vérification', verifyPh: 'npm test · go test ./... (optionnel)', model: 'Modèle', modelPh: 'claude-opus-4-5 (optionnel)',
|
||||
searchPh: '⌕ Rechercher…', clearFilter: '✕ Effacer les filtres', matchCount: '{n} résultats',
|
||||
fgroups: { todo: 'À faire', doing: 'En cours', gate: 'À approuver', bad: 'Anomalies', hold: 'En attente', done: 'Terminées' },
|
||||
fStatus: 'Statut', fAssignee: 'Exécutant', assignees: { agent: 'Agent', human: 'Humain' },
|
||||
fullRead: 'Plein écran', depsLabel: 'DEPS (toutes requises)', depDone: 'terminée', depWait: 'en attente',
|
||||
gateSection: "Portes d'approbation · en attente", accept: '✓ Accepter', reject: '✕ Rejeter', confirmReject: 'Confirmer le rejet',
|
||||
gateSection: "Portes d'approbation", accept: '✓ Accepter', reject: '✕ Rejeter', confirmReject: 'Confirmer le rejet',
|
||||
rejectPlaceholder: "Retour d'amélioration (obligatoire)",
|
||||
taskSection: 'Arbre des tâches', newTask: '+ Nouvelle tâche', titleLabel: 'Titre', titlePlaceholder: 'Que faut-il faire',
|
||||
complexity: 'Complexité', cplxAuto: 'AUTO', parentTask: 'Parent', topLevel: '(racine)', priority: 'Priorité',
|
||||
|
||||
Reference in New Issue
Block a user