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
This commit is contained in:
126
apps/docs/src/components/kanban/DiffModal.tsx
Normal file
126
apps/docs/src/components/kanban/DiffModal.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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)',
|
||||
};
|
||||
Reference in New Issue
Block a user