// 左栏: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.gMaxRetries, )} {field(t.gTimeoutMin, )} {field(t.gAutoPlan, )} {field(t.gAutoExec, )} {field(t.gBudget, )} {field(t.gBudgetPeriod, )}
); } 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) => (
{v}
{k}
))}
{/* 按项目(默认 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;