// 左栏: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 ; }
// 全局设置弹框:用户级默认配置(新建项目套用)。保存 / 取消,落库经 onSave。
function SettingsModal({ settings, onSave, onClose, t }) {
const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
const s = settings || {};
const [autonomy, setAutonomy] = React.useState(s.autonomy || 'manual');
const [concurrency, setConcurrency] = React.useState(String(s.concurrency ?? 1));
const [maxRetries, setMaxRetries] = React.useState(String(s.maxRetries ?? 2));
const [timeoutMin, setTimeoutMin] = React.useState(String(Math.round((s.timeoutMs ?? 1800000) / 60000)));
const [autoPlan, setAutoPlan] = React.useState(!!s.autoApprovePlan);
const [autoExec, setAutoExec] = React.useState(!!s.autoApproveExec);
const [budgetUsd, setBudgetUsd] = React.useState(s.budgetUsd != null ? String(s.budgetUsd) : '');
const [budgetPeriod, setBudgetPeriod] = React.useState(s.budgetPeriod || 'month');
const [model, setModel] = React.useState(s.model || '');
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
const submit = () => {
onSave({
autonomy, concurrency: Number(concurrency) || 1, maxRetries: Number(maxRetries) || 0,
timeoutMs: Math.max(1, Number(timeoutMin) || 30) * 60000,
autoApprovePlan: autoPlan, autoApproveExec: autoExec,
budgetUsd: budgetUsd.trim() === '' ? null : Number(budgetUsd),
budgetPeriod, model: model.trim() === '' ? null : model.trim(),
});
onClose();
};
const Toggle = ({ on, set }) => (
);
const field = (label, node) => (
);
return (
▍{t.gGlobalSettings}
{t.gSettingsHint}
{field(t.workMode,
{field(t.model, )}
);
}
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 = (
{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}
{[
{ 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) => (
{ e.currentTarget.style.background = 'var(--panel-2)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{it.label}
))}
) : null}
{showSettings ?
setShowSettings(false)} t={t} /> : 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);
}
// 全局 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 (
{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) => (
))}
{/* 按项目(默认 3,多余折叠,按活跃排序)*/}
{t.apPerProj}
{shown.map((p) => (
{p.name[0].toUpperCase()}
{p.name}
{p.active > 0 ? (
{p.active}
) : {t.apIdle}}
{fmtTokens(p.tokens)}
{p.runs} {t.apRuns}
))}
{projs.length > 3 ? (
) : 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, 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 (
);
}
window.MaestroKitSidebar = Sidebar;