1c62059424
- 全局 AGENT 卡片精简为 2×2 概览(近 7 天窗口 + 真实运行数),删按项目列表,加「详情」弹框(按天/周/月柱状图 + 分项目下钻表);- 项目列表展开态封顶 6,多余折叠;- plan/exec 评审弹框:markdown 折叠卡片 + 点文件看 diff;- live 输出流提取 tool_use/tool_result;- Agent/审批闸/任务树/已归档标题栏滚动置顶。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
187 lines
11 KiB
React
187 lines
11 KiB
React
// 已归档区(done / 取消,时间倒序分页)+ 归档详情模态
|
||
function ArchiveRow({ item, t, onOpen }) {
|
||
const { ComplexityBadge, StatusChip } = window.MaestroDesignSystem_a6a290;
|
||
const [hover, setHover] = React.useState(false);
|
||
return (
|
||
<div onClick={() => onOpen(item)} title={t.archiveTip}
|
||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer',
|
||
padding: '7px 12px', fontSize: 12.5,
|
||
borderBottom: '1px dashed var(--line-soft)',
|
||
background: hover ? 'var(--panel)' : 'transparent',
|
||
}}>
|
||
<span style={{ color: 'var(--muted)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{item.title}<span style={{ color: 'var(--faint)', fontSize: 10.5, marginLeft: 6 }}>{item.id}</span>
|
||
</span>
|
||
{item.subs ? <span style={{ flex: 'none', fontSize: 10.5, color: 'var(--faint)' }}>{t.archiveSubs.replace('{n}', item.subs)}</span> : null}
|
||
<span style={{ flex: 1, minWidth: 8 }}></span>
|
||
<ComplexityBadge complexity={item.cplx} />
|
||
<StatusChip status={item.status} label={t.status[item.status]} />
|
||
<span style={{ flex: 'none', fontSize: 11, color: 'var(--faint)' }} title={item.timeFull}>{item.time}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SizeSelect({ size, setSize, t }) {
|
||
const [open, setOpen] = React.useState(false);
|
||
const [hover, setHover] = 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]);
|
||
return (
|
||
<span ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
|
||
<button onClick={() => setOpen(!open)}
|
||
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
||
style={{
|
||
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.04em',
|
||
background: 'transparent', color: hover || open ? 'var(--green)' : 'var(--muted)',
|
||
border: '1px solid ' + (hover || open ? 'var(--green-dim)' : 'var(--line)'),
|
||
borderRadius: 'var(--radius-sm, 4px)', padding: '3px 9px', cursor: 'pointer',
|
||
display: 'inline-flex', alignItems: 'center', gap: 7, transition: 'color .12s, border-color .12s',
|
||
}}>
|
||
{t.perPage} <b style={{ color: 'var(--green)', fontSize: 11.5 }}>{size}</b>
|
||
<span style={{ fontSize: 8, color: 'var(--faint)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}>▼</span>
|
||
</button>
|
||
{open ? (
|
||
<div style={{
|
||
position: 'absolute', bottom: 'calc(100% + 5px)', right: 0, zIndex: 60,
|
||
display: 'grid', minWidth: '100%',
|
||
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',
|
||
}}>
|
||
{[10, 20, 50, 100].map((n) => {
|
||
const active = n === size;
|
||
return (
|
||
<button key={n} onClick={() => { setSize(n); setOpen(false); }} style={{
|
||
fontFamily: 'var(--mono)', fontSize: 11.5, fontWeight: active ? 700 : 400, textAlign: 'left',
|
||
background: active ? 'var(--panel-2)' : 'transparent',
|
||
color: active ? 'var(--green)' : 'var(--ink)',
|
||
border: 'none', borderRadius: 4, padding: '6px 12px 6px 8px', cursor: 'pointer',
|
||
display: 'flex', alignItems: 'center', gap: 7,
|
||
}}
|
||
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
|
||
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
|
||
<span style={{ width: 8, color: 'var(--green)', fontSize: 10 }}>{active ? '▍' : ''}</span>{n}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
) : null}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function ArchivePager({ page, pages, size, setPage, setSize, t }) {
|
||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', justifyContent: 'center', fontFamily: 'var(--mono)' }}>
|
||
{pages > 1 ? (
|
||
<React.Fragment>
|
||
<Button size="xs" disabled={page <= 1} onClick={() => setPage(page - 1)}>{t.pagePrev}</Button>
|
||
<span style={{ fontSize: 11, color: 'var(--muted)' }}>{t.pageOf.replace('{a}', page).replace('{b}', pages)}</span>
|
||
<Button size="xs" disabled={page >= pages} onClick={() => setPage(page + 1)}>{t.pageNext}</Button>
|
||
</React.Fragment>
|
||
) : null}
|
||
<SizeSelect size={size} setSize={setSize} t={t} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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={{ opacity: .82 }}>
|
||
<window.MaestroKitSectionHead icon="archive" title={t.archive} count={items.length} accent="var(--faint)" folded={folded} onToggleFold={toggleFold} t={t} sticky />
|
||
{!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>
|
||
);
|
||
}
|
||
|
||
// 归档详情模态(只读:属性 / 产出 / 执行历史 / 结果 / 审批 / 时间线)
|
||
function ArchiveModal({ item, t, onClose }) {
|
||
const { ComplexityBadge, StatusChip, Timeline } = window.MaestroDesignSystem_a6a290;
|
||
React.useEffect(() => {
|
||
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose]);
|
||
if (!item) return null;
|
||
const d = item.detail || {};
|
||
const label = (text) => <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '14px 0 4px' }}>{text}</div>;
|
||
const doc = (text) => (
|
||
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)' }}>{text}</pre>
|
||
);
|
||
const kvGrid = { fontSize: 12, display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '3px 12px', margin: 0 };
|
||
return (
|
||
<div style={{ position: 'fixed', inset: 0, zIndex: 1100, display: 'grid', placeItems: 'center' }}>
|
||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.78)', backdropFilter: 'blur(2px)' }}></div>
|
||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', width: 'min(860px, 94vw)', maxHeight: '92vh', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 'var(--radius-lg, 10px)', overflow: 'hidden', boxShadow: '0 24px 64px rgba(0,0,0,.6)', animation: 'maestro-rise .18s ease both' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', padding: '12px 16px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)' }}>
|
||
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg-deep)', background: 'var(--muted)', padding: '2px 8px', borderRadius: 3 }}>{t.archiveDetail}</span>
|
||
<span style={{ fontWeight: 700, fontSize: 15 }}>{item.title}</span>
|
||
<ComplexityBadge complexity={item.cplx} />
|
||
<StatusChip status={item.status} label={t.status[item.status]} />
|
||
<span style={{ flex: 1 }}></span>
|
||
<button onClick={onClose} title={t.closeTip} style={{ fontFamily: 'var(--mono)', fontSize: 13, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '4px 8px' }}>✕</button>
|
||
</div>
|
||
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 20px 24px', fontFamily: 'var(--mono)' }}>
|
||
{d.attrs ? <dl style={kvGrid}>{d.attrs.map(([k, v], i) => (
|
||
<React.Fragment key={i}><dt style={{ color: 'var(--muted)' }}>{k}</dt><dd style={{ margin: 0, wordBreak: 'break-all' }}>{v}</dd></React.Fragment>
|
||
))}</dl> : null}
|
||
{d.spec ? <React.Fragment>{label('SPEC · 方案')}{doc(d.spec)}</React.Fragment> : null}
|
||
{d.ops ? <React.Fragment>{label('OPERATIONS · 操作')}{doc(d.ops)}</React.Fragment> : null}
|
||
{d.runs && d.runs.length ? <React.Fragment>
|
||
{label(t.runsLabel + '(' + d.runs.length + ')')}
|
||
<dl style={kvGrid}>{d.runs.map((r, i) => (
|
||
<React.Fragment key={i}>
|
||
<dt style={{ color: 'var(--muted)' }}>{r.kind} · {r.status}</dt>
|
||
<dd style={{ margin: 0 }}>{r.span}{r.ref ? <span style={{ display: 'block', color: 'var(--faint)', fontSize: 10.5 }}>{r.ref}</span> : null}</dd>
|
||
</React.Fragment>
|
||
))}</dl>
|
||
</React.Fragment> : null}
|
||
{d.result ? <React.Fragment>
|
||
{label(t.resultLabel)}
|
||
<dl style={kvGrid}>
|
||
<dt style={{ color: 'var(--muted)' }}>{t.branch}</dt><dd style={{ margin: 0 }}>{d.result.branch}</dd>
|
||
<dt style={{ color: 'var(--muted)' }}>commits</dt><dd style={{ margin: 0 }}>{d.result.commits.join(' · ')}</dd>
|
||
<dt style={{ color: 'var(--muted)' }}>diff</dt><dd style={{ margin: 0 }}>{d.result.diff}</dd>
|
||
</dl>
|
||
</React.Fragment> : null}
|
||
{d.approvals && d.approvals.length ? <React.Fragment>
|
||
{label(t.approvalsLabel)}
|
||
<dl style={kvGrid}>{d.approvals.map((a, i) => (
|
||
<React.Fragment key={i}>
|
||
<dt style={{ color: a.action === 'accept' ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>{a.gate} · {a.action === 'accept' ? t.accepted : t.rejected}</dt>
|
||
<dd style={{ margin: 0 }}>{t.approver} {a.actor} · {a.at}{a.reason ? <span style={{ display: 'block', color: 'var(--amber)' }}>↳ {a.reason}</span> : null}</dd>
|
||
</React.Fragment>
|
||
))}</dl>
|
||
</React.Fragment> : null}
|
||
{d.timeline && d.timeline.length ? <React.Fragment>
|
||
{label(t.timelineLabel)}
|
||
<Timeline items={d.timeline} />
|
||
</React.Fragment> : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
Object.assign(window, { MaestroKitArchiveSection: ArchiveSection, MaestroKitArchiveModal: ArchiveModal });
|