- RunEventList: grouped activity timeline. Assistant text becomes chat bubbles (auto-collapsing long messages); tool_start/tool_end pair into entries with spinners and expandable input/result blocks; bevy output rolls into a live console; relative timestamps on a left rail - AgentRunBar: redesigned as a mission console. Live stats header (elapsed time, tool count, events), animated status banner with sweep/glow while running, clearer action bar. All controls preserved (run/steer/stop, diff/merge/bevy) so the human-only merge/complete safety model holds - tailwind.css: vn-flow, vn-sweep, vn-dots, vn-spin keyframes - CardModal: full card overlay (orchestrator, references, tags, comments) - DiffModal: branch-diff review (commits, stat, capped patch) - useOrchestrator: background polling + bevy status sync + ref-counted SSE - KanbanCard: pulsing agent/bevy running badge on collapsed cards
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import type { DiffResult } from '../../lib/orchestratorApi';
|
|
|
|
/**
|
|
* Overlay showing a run's branch diff against main — the commits it would
|
|
* bring in, a `--stat` summary, and the full (size-capped) patch — so the
|
|
* operator can review an agent's work before merging it.
|
|
*/
|
|
|
|
interface DiffModalProps {
|
|
/** Fetch the diff; throws on a missing worktree. */
|
|
load: () => Promise<DiffResult>;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function DiffModal({ load, onClose }: DiffModalProps) {
|
|
const [diff, setDiff] = useState<DiffResult | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setError(null);
|
|
setDiff(null);
|
|
load()
|
|
.then((d) => {
|
|
if (!cancelled) setDiff(d);
|
|
})
|
|
.catch((e) => {
|
|
if (!cancelled) setError(e instanceof Error ? e.message : 'Failed to load diff');
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [load]);
|
|
|
|
return (
|
|
<div style={overlayStyle} onClick={onClose}>
|
|
<div
|
|
className="max-h-[88vh] w-full max-w-4xl overflow-hidden rounded-2xl border border-border bg-surface p-6 shadow-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
style={{ display: 'flex', flexDirection: 'column' }}
|
|
>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="m-0 text-lg">Branch Diff</h2>
|
|
<button
|
|
className="cursor-pointer rounded border border-border bg-transparent px-2 py-0.5 text-muted hover:text-fg"
|
|
onClick={onClose}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="rounded-lg border border-red/25 bg-red/8 px-3 py-2 font-mono text-[0.78rem] text-red">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
{!error && !diff && (
|
|
<p className="font-mono text-sm text-muted">Loading diff…</p>
|
|
)}
|
|
|
|
{diff && (
|
|
<div className="flex min-h-0 flex-col gap-4">
|
|
{diff.branch && (
|
|
<div className="font-mono text-[0.7rem] text-muted">
|
|
⎇ <span className="text-fg-dim">{diff.branch}</span> → main
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h3 className="m-0 mb-1 text-[0.75rem] uppercase tracking-wider text-muted">
|
|
Commits ({diff.commits.length})
|
|
</h3>
|
|
{diff.commits.length === 0 ? (
|
|
<p className="m-0 font-mono text-[0.72rem] text-muted">No commits ahead — already merged or empty.</p>
|
|
) : (
|
|
<ul className="m-0 list-none p-0 font-mono text-[0.72rem]">
|
|
{diff.commits.map((c) => (
|
|
<li key={c.sha} className="mb-0.5">
|
|
<span className="text-accent">{c.sha}</span>{' '}
|
|
<span className="text-fg-dim">{c.subject}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{diff.stat && (
|
|
<div>
|
|
<h3 className="m-0 mb-1 text-[0.75rem] uppercase tracking-wider text-muted">Changes</h3>
|
|
<pre className="m-0 overflow-x-auto rounded-lg border border-border bg-bg p-3 font-mono text-[0.68rem] leading-relaxed text-fg-dim">
|
|
{diff.stat}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
|
|
<div className="min-h-0 flex-1">
|
|
<h3 className="m-0 mb-1 text-[0.75rem] uppercase tracking-wider text-muted">
|
|
Patch {diff.truncated && <span style={{ color: 'var(--accent)' }}>(truncated)</span>}
|
|
</h3>
|
|
<pre
|
|
className="m-0 max-h-[44vh] overflow-auto rounded-lg border border-border bg-bg p-3 font-mono text-[0.66rem] leading-relaxed"
|
|
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
|
|
>
|
|
{diff.patch || '(no textual changes)'}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const overlayStyle: React.CSSProperties = {
|
|
position: 'fixed',
|
|
inset: 0,
|
|
background: 'rgba(0,0,0,0.6)',
|
|
backdropFilter: 'blur(2px)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 50,
|
|
padding: 'var(--sp-4)',
|
|
};
|