// 左栏:logo + 项目列表(git 图标)+ WS 状态脚 · 可折叠(折叠后仅图标)
// 项目 logo:图片优先(原型无图),回退首字母色块 hsl(h 45% 60%)
function ProjectLogo({ project, size = 26 }) {
return (
{(project.name || '?')[0].toUpperCase()}
);
}
function MaestroGitIcon({ size = 16 }) {
return (
);
}
// 像素章鱼 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 ;
}
// 项目状态:运行中(青脉冲) / 暂停(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 (
);
}
// 侧栏底部小行:图标 + 主文 + 副文(折叠时仅图标,hover 高亮)
function SideRow({ icon, label, detail, onClick, collapsed, title, accent }) {
const interactive = !!onClick;
return (
{ e.currentTarget.style.background = 'var(--panel)'; } : undefined}
onMouseLeave={interactive ? (e) => { e.currentTarget.style.background = 'transparent'; } : undefined}>
{icon}
{!collapsed ? (
{label}
{detail ? {detail} : null}
) : null}
{!collapsed && interactive ? › : null}
);
}
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 ; }
function IcoGlobalCfg() { return ; }
function UserRow({ user, t, collapsed }) {
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); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const avatar = (
{user.initial}
);
return (
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 ? (
{user.name}
{user.plan}
▴
) : null}
{open ? (
{user.name} {user.handle}
{t.gUserPlan} · {user.plan}
{[t.gUserSettings, t.gUserSignOut].map((label, i) => (
{ e.currentTarget.style.background = 'var(--panel-2)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{label}
))}
) : null}
);
}
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);
}
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);
return (
} accent="var(--cyan)" onClick={toggle}
label={t.gAgents} detail={detail} title={t.gAgents + ' · ' + detail} />
{open && pos ? (
▍{t.gAgents.toUpperCase()}
{t.apWeek}
{/* 总结 */}
{[
[fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'],
['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'],
[summary.runsWeek, t.apRuns, 'var(--ink)'],
[summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'],
].map(([v, k, c], i) => (
))}
{/* 按项目 */}
{t.apPerProj}
{summary.byProject.map((p) => (
{p.name[0].toUpperCase()}
{p.name}
{p.active > 0 ? (
{p.active}
) : {t.apIdle}}
{fmtTokens(p.tokens)}
{p.runs} {t.apRuns}
))}
) : null}
);
}
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) => (
{k}
{v}
);
return (
} onClick={toggle}
label={t.gConfig}
detail={t.gConfigDetail.replace('{n}', global.maxAgents).replace('{v}', global.daemon)}
title={t.gConfig} />
{open && pos ? (
▍{t.gConfig.toUpperCase()}
{stat('daemon', global.daemon, 'var(--green)')}
{stat('port', ':' + global.port)}
{stat('uptime', global.uptime)}
{stat(t.gAgents, global.runningAgents + ' / ' + global.maxAgents, 'var(--cyan)')}
) : null}
);
}
function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig }) {
const { Button } = window.MaestroDesignSystem_a6a290;
return (
);
}
window.MaestroKitSidebar = Sidebar;