Files
maestro/design/ui_kits/console/Sidebar.jsx
T
wangjia 8765e61498 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>
2026-06-29 11:30:32 +08:00

484 lines
29 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 左栏:logo + 项目列表(git 图标)+ WS 状态脚 · 可折叠(折叠后仅图标)
// 项目 logo:图片优先(原型无图),回退首字母色块 hsl(h 45% 60%)
function ProjectLogo({ project, size = 26 }) {
return (
<span style={{
flex: 'none', width: size, height: size, borderRadius: 5,
display: 'grid', placeItems: 'center',
fontSize: size * 0.5, fontWeight: 700, color: '#0a0d0b',
background: 'hsl(' + (project.hue || 140) + ' 45% 60%)',
}}>{(project.name || '?')[0].toUpperCase()}</span>
);
}
function MaestroGitIcon({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none' }}>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
);
}
// 像素章鱼 mark(折叠态 logo
function MaestroMark({ scale = 2.2 }) {
const ref = React.useRef(null);
React.useEffect(() => {
const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.'];
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
const ctx = ref.current.getContext('2d');
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
}));
}, []);
return <canvas ref={ref} width="15" height="12"
style={{ width: 15 * scale, height: 12 * scale, imageRendering: 'pixelated', filter: 'drop-shadow(0 0 6px rgba(95,221,125,.4))' }} />;
}
// 项目状态:运行中(青脉冲) / 暂停(faint) / 阻塞(琥珀) / 空闲(muted)
function projStateMeta(state, t) {
return ({
running: { color: 'var(--cyan)', pulse: true, label: t.projRunning },
paused: { color: 'var(--faint)', pulse: false, label: t.projPaused },
blocked: { color: 'var(--amber)', pulse: true, label: t.projBlocked },
idle: { color: 'var(--muted)', pulse: false, label: t.projIdle },
})[state] || { color: 'var(--muted)', pulse: false, label: t.projIdle };
}
function ProjStatusDot({ meta, size = 7 }) {
return (
<span style={{
flex: 'none', width: size, height: size, borderRadius: '50%', background: meta.color,
boxShadow: '0 0 7px ' + meta.color,
animation: meta.pulse ? 'maestro-pulse 1.4s infinite' : 'none',
}}></span>
);
}
// 侧栏底部小行:图标 + 主文 + 副文(折叠时仅图标,hover 高亮)
function SideRow({ icon, label, detail, onClick, collapsed, title, accent }) {
const interactive = !!onClick;
return (
<div onClick={onClick} title={title || label}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: collapsed ? '9px 0' : '8px 12px', minHeight: 40,
justifyContent: collapsed ? 'center' : 'flex-start',
cursor: interactive ? 'pointer' : 'default', transition: 'background .1s',
}}
onMouseEnter={interactive ? (e) => { e.currentTarget.style.background = 'var(--panel)'; } : undefined}
onMouseLeave={interactive ? (e) => { e.currentTarget.style.background = 'transparent'; } : undefined}>
<span style={{ flex: 'none', color: accent || 'var(--muted)', display: 'inline-flex' }}>{icon}</span>
{!collapsed ? (
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{label}</span>
{detail ? <span style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{detail}</span> : null}
</span>
) : null}
{!collapsed && interactive ? <span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11 }}></span> : null}
</div>
);
}
const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none' } };
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>; }
// 全局设置弹框:用户级默认配置(新建项目套用)。保存 / 取消,落库经 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;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const avatar = (
<span style={{
flex: 'none', width: 26, height: 26, borderRadius: '50%', display: 'grid', placeItems: 'center',
fontSize: 12, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + (user.hue || 200) + ' 50% 62%)',
}}>{user.initial}</span>
);
return (
<div ref={ref} style={{ position: 'relative', borderTop: '1px solid var(--line-soft)' }}>
<div onClick={() => setOpen(!open)} title={user.name + ' · ' + user.plan}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: collapsed ? '10px 0' : '10px 12px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', transition: 'background .1s' }}
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
{avatar}
{!collapsed ? (
<React.Fragment>
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{user.name}</span>
<span style={{ fontSize: 10, color: 'var(--green)', whiteSpace: 'nowrap' }}>{user.plan}</span>
</span>
<span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}></span>
</React.Fragment>
) : null}
</div>
{open ? (
<div style={{
position: collapsed ? 'fixed' : 'absolute',
...(collapsed
? { left: 58, bottom: 14, width: 180 }
: { bottom: 'calc(100% + 4px)', left: 12, right: 12, minWidth: 160 }),
zIndex: 80,
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
boxShadow: '0 10px 30px rgba(0,0,0,.65)', padding: 6, animation: 'maestro-rise .12s ease both',
}}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--line-soft)', marginBottom: 4 }}>
<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>
{[
{ 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'; }}>{it.label}</div>
))}
</div>
) : null}
{showSettings ? <SettingsModal settings={settings} onSave={onSaveSettings} onClose={() => setShowSettings(false)} t={t} /> : null}
</div>
);
}
function fmtTokens(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1e3) return Math.round(n / 1e3) + 'K';
return String(n);
}
// 全局 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={{
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>
{/* 总结 */}
<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>
{/* 按项目(默认 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>
{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>
);
}
function GlobalConfigRow({ global, t, collapsed }) {
const { Select } = window.MaestroDesignSystem_a6a290;
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 = 250;
// 优先在行上方对齐左缘;折叠态贴侧栏右侧
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 stat = (k, v, accent) => (
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, padding: '5px 0', borderBottom: '1px solid var(--line-soft)' }}>
<span style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.06em' }}>{k}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: accent || 'var(--ink)' }}>{v}</span>
</div>
);
return (
<div style={{ position: 'relative' }}>
<div ref={rowRef}>
<SideRow collapsed={collapsed} icon={<IcoGlobalCfg />} onClick={toggle}
label={t.gConfig}
detail={t.gConfigDetail.replace('{n}', global.maxAgents).replace('{v}', global.daemon)}
title={t.gConfig} />
</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: 8 }}>
<span style={{ color: 'var(--cyan)' }}></span>{t.gConfig.toUpperCase()}
</div>
<div style={{ marginBottom: 12 }}>
{stat('daemon', global.daemon, 'var(--green)')}
{stat('port', ':' + global.port)}
{stat('uptime', global.uptime)}
{stat(t.gAgents, global.runningAgents + ' / ' + global.maxAgents, 'var(--cyan)')}
</div>
<div style={{ display: 'grid', gap: 10 }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
<span>{t.maxConcurrency}</span>
<Select defaultValue={String(global.maxAgents)} style={{ width: '100%' }} options={['1', '2', '4', '6', '8'].map((n) => ({ value: n, label: n }))} />
</label>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
<span>{t.workMode}</span>
<Select defaultValue={global.autonomy} style={{ width: '100%' }} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />
</label>
</div>
</div>
) : null}
</div>
);
}
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' }}>
{collapsed ? <MaestroMark /> : (
<React.Fragment>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<MaestroMark scale={1.8} />
<span style={{ fontSize: 19, fontWeight: 700, letterSpacing: '.28em', color: 'var(--green)', textShadow: '0 0 12px rgba(95,221,125,.45)' }}>
MAESTRO<span style={{ marginLeft: 1, animation: 'maestro-blink 1.1s steps(1) infinite' }}></span>
</span>
</div>
<div style={{ marginTop: 5, fontSize: 9, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.2em', lineHeight: 1.7, textTransform: 'uppercase' }}>{t.logoSub}</div>
</React.Fragment>
)}
</div>
{!collapsed ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, fontWeight: 600, letterSpacing: '.22em', color: 'var(--muted)', padding: '16px 16px 8px' }}>
<span style={{ color: 'var(--green)' }}></span>{t.projects}
<span style={{ marginLeft: 'auto' }}><Button variant="ghost" size="xs" onClick={onNewProject}>{t.newProject}</Button></span>
</div>
) : <div style={{ height: 12 }}></div>}
<ul style={{ listStyle: 'none', flex: 1, margin: 0, padding: 0 }}>
{projects.map((p) => {
const active = p.id === currentId;
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: 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 && !dragId) e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
<ProjectLogo project={p} size={collapsed ? 26 : 24} />
{!collapsed ? (
<React.Fragment>
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontWeight: 600, fontSize: 13, color: active ? 'var(--green)' : 'var(--ink)' }}>{p.name}</span>
<span style={{ fontSize: 10.5, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.path}</span>
</span>
<span style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
{p.pending > 0 ? (
<span title={t.projPendingTip.replace('{n}', p.pending)} style={{
fontFamily: 'var(--mono)', fontSize: 9.5, fontWeight: 700, lineHeight: '15px',
minWidth: 15, height: 15, textAlign: 'center', padding: '0 4px',
color: 'var(--bg-deep)', background: 'var(--violet)', borderRadius: 8,
}}>{p.pending}</span>
) : null}
<ProjStatusDot meta={meta} />
</span>
</React.Fragment>
) : (
<span style={{ position: 'absolute', top: 7, right: 9, display: 'flex', alignItems: 'center' }}>
<ProjStatusDot meta={meta} size={p.pending > 0 ? 8 : 6} />
</span>
)}
</li>
);
})}
</ul>
{/* ── 全局 AGENT 卡片(常驻)+ 用户(全局设置已并入用户菜单)── */}
<div style={{ borderTop: '1px solid var(--line-soft)' }}>
<AgentCard global={global} summary={summary} t={t} collapsed={collapsed} />
</div>
<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>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 8px var(--green)', display: 'inline-block', flex: 'none' }}></span>
<span style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{t.connected}</span>
</React.Fragment>
) : null}
<button onClick={onToggleCollapse} title={collapsed ? t.expandSide : t.collapseSide}
style={{
marginLeft: collapsed ? 0 : 'auto', flex: 'none', cursor: 'pointer',
fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1,
background: 'transparent', color: 'var(--muted)',
border: '1px solid var(--line)', borderRadius: 'var(--radius-sm, 4px)', padding: '4px 7px',
}}
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)'; }}>
{collapsed ? '»' : '«'}
</button>
</div>
</aside>
);
}
window.MaestroKitSidebar = Sidebar;