Files
Space-Game/apps/docs/src/components/kanban/KanbanCard.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

209 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Card, Column, DocPage } from '../../lib/kanbanApi';
import type { CustomPage } from '../../lib/pagesApi';
import { ReferenceLinks } from './ReferenceLinks';
import type { UseOrchestrator } from './useOrchestrator';
const COLUMN_DOT: Record<Column, string> = {
done: 'var(--green)',
'in-progress': 'var(--accent)',
todo: 'var(--red)',
backlog: 'var(--muted)',
};
/**
* Compact, click-to-open kanban card.
*
* The full detail (agent orchestration, comments, references, tags) lives in
* the CardModal opened by `onOpen`. This card is a dense preview: status, id,
* title, description, references, and a prominent live indicator while an agent
* or Bevy playtest is running on the card's worktree.
*/
interface KanbanCardProps {
card: Card;
pages: DocPage[];
customPages: CustomPage[];
orch: UseOrchestrator;
onOpen: () => void;
}
export function KanbanCard({ card, pages, customPages, orch, onOpen }: KanbanCardProps) {
const agentRunning = orch.isRunning(card.id);
const run = orch.runForCard(card.id);
const bevyRunning = Boolean(run && orch.bevyIsRunning(run.id));
const active = agentRunning || bevyRunning;
return (
<div
style={{
...cardStyle,
// An active card is unmistakable: accent glow + tinted background +
// colored left rail so a running agent is visible at a glance, even
// from across the board.
...(active
? {
borderColor: 'var(--accent)',
boxShadow: '0 0 0 1px var(--accent), 0 0 14px rgba(240,160,48,0.25)',
background: 'rgba(240,160,48,0.04)',
}
: {}),
borderLeft: `3px solid ${COLUMN_DOT[card.status]}`,
}}
onClick={onOpen}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onOpen();
}
}}
title="Open card details"
>
{/* Header: id + category + counts */}
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: 'var(--sp-2)' }}>
<span style={badgeStyle}>{card.id}</span>
<span style={categoryStyle}>{card.category}</span>
{active && (
<span style={{ marginLeft: 'auto' }}>
<RunningBadge agent={agentRunning} bevy={bevyRunning} />
</span>
)}
{!active && (
<span style={{ marginLeft: 'auto', fontSize: '0.7rem', color: 'var(--fg-dim)' }}>
{card.comments.length > 0 && `💬 ${card.comments.length} `}
{card.tags.length > 0 && `🏷️ ${card.tags.length} `}
{card.references.length > 0 && `🔗 ${card.references.length}`}
</span>
)}
</div>
<h4 style={titleStyle}>{card.title}</h4>
<p style={descStyle}>{card.description}</p>
{/* References (compact) */}
<div style={{ marginTop: 'var(--sp-2)', marginBottom: 'var(--sp-1)' }}>
<ReferenceLinks references={card.references} pages={pages} customPages={customPages} compact />
</div>
{/* Footer: files + notes + tags (read-only chips here) */}
{(card.files || card.notes || card.tags.length > 0) && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', marginTop: 'var(--sp-1)' }}>
{card.files && (
<div style={{ fontSize: '0.65rem', color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
📁 {card.files}
</div>
)}
{card.notes && <div style={noteStyle}>💡 {card.notes}</div>}
{card.tags.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{card.tags.map((tag) => (
<span key={tag} style={tagStyle}>
🏷 {tag}
</span>
))}
</div>
)}
</div>
)}
</div>
);
}
/** Live activity badge: pulses while an agent or Bevy run is in flight. */
function RunningBadge({ agent, bevy }: { agent: boolean; bevy: boolean }) {
const color = agent ? 'var(--accent)' : 'var(--purple)';
const label = agent ? 'Agent' : 'Bevy';
return (
<span
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
fontSize: '0.62rem',
fontWeight: 600,
padding: '2px 7px',
borderRadius: 'var(--radius-pill)',
border: `1px solid ${color}`,
background: `${color}1a`,
color,
whiteSpace: 'nowrap',
textTransform: 'uppercase',
letterSpacing: '0.04em',
}}
>
<span style={pulseDotStyle(color)} />
{label} running
</span>
);
}
function pulseDotStyle(color: string): React.CSSProperties {
return {
width: '6px',
height: '6px',
borderRadius: '50%',
background: color,
boxShadow: `0 0 6px ${color}`,
animation: 'vn-pulse 1.1s ease-in-out infinite',
};
}
// --- styles ----------------------------------------------------------------
const cardStyle: React.CSSProperties = {
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
padding: 'var(--sp-3)',
cursor: 'pointer',
transition: 'all 0.15s ease',
};
const badgeStyle: React.CSSProperties = {
fontFamily: 'var(--font-mono)',
fontSize: '0.65rem',
color: 'var(--muted)',
background: 'var(--surface-raised)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
};
const categoryStyle: React.CSSProperties = {
fontSize: '0.7rem',
color: 'var(--fg-dim)',
background: 'rgba(100,100,100,0.1)',
padding: '2px 6px',
borderRadius: 'var(--radius-sm)',
};
const titleStyle: React.CSSProperties = {
margin: '0 0 var(--sp-1) 0',
fontSize: '0.9rem',
fontWeight: 500,
color: 'var(--fg)',
};
const descStyle: React.CSSProperties = {
margin: 0,
fontSize: '0.8rem',
color: 'var(--fg-dim)',
lineHeight: 1.4,
};
const noteStyle: React.CSSProperties = {
fontSize: '0.7rem',
color: 'var(--accent)',
padding: '4px 8px',
background: 'rgba(240,160,48,0.08)',
borderRadius: 'var(--radius-sm)',
borderLeft: '2px solid var(--accent)',
};
const tagStyle: React.CSSProperties = {
fontSize: '0.65rem',
padding: '2px 6px',
background: 'rgba(100,150,255,0.1)',
border: '1px solid rgba(100,150,255,0.3)',
borderRadius: 'var(--radius-sm)',
};