Files
Space-Game/apps/docs/src/components/kanban/DiffModal.tsx
francy51 72a41c2d76 feat(kanban): card detail modal and rich agent-run console
- 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
2026-06-16 18:17:35 -04:00

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)',
};